{"openapi":"3.0.0","info":{"title":"HIYD Backend API","version":"0.1.0","description":"Node.js Express backend for HIYD health data","contact":{"name":"API Support","email":"support@example.com"}},"servers":[{"url":"https://api.hiyd.ai"}],"tags":[{"name":"Health","description":"Health check endpoints"},{"name":"Sync","description":"Data synchronization endpoints"},{"name":"AI","description":"AI-powered suggestion endpoints"},{"name":"Stripe","description":"Stripe payment gateway endpoints"},{"name":"Subscriptions","description":"Subscription and plan management"}],"components":{"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string","description":"Error message"},"details":{"type":"string","description":"Additional error details"}}},"HealthCheck":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"now":{"type":"string","format":"date-time","example":"2024-01-01T12:00:00.000Z"}}},"SyncRequest":{"type":"object","required":["deviceId"],"properties":{"deviceId":{"type":"string","description":"Unique device identifier","example":"DEV-WEARABLE-001"},"batchClientId":{"type":"string","description":"Optional batch client identifier","example":"batch-456"},"timestamp":{"type":"string","format":"date-time","description":"Timestamp of the telemetry data","example":"2025-10-16T08:26:15Z"},"ppg":{"type":"object","description":"Photoplethysmography data","properties":{"heartRate":{"type":"number","description":"Heart rate in BPM","example":80},"spO2":{"type":"number","description":"Blood oxygen saturation percentage","example":98}}},"bloodPressure":{"type":"object","description":"Blood pressure measurements","properties":{"systolic_mmHg":{"type":"number","description":"Systolic pressure in mmHg","example":118},"diastolic_mmHg":{"type":"number","description":"Diastolic pressure in mmHg","example":76}}},"imu":{"type":"object","description":"Inertial Measurement Unit data","properties":{"accelerometer":{"type":"object","description":"Accelerometer readings in g","properties":{"x":{"type":"number","example":0.02},"y":{"type":"number","example":-0.09},"z":{"type":"number","example":0.98}}},"gyroscope":{"type":"object","description":"Gyroscope readings in degrees/second","properties":{"x":{"type":"number","example":-1.5},"y":{"type":"number","example":1.1},"z":{"type":"number","example":-0.2}}}}},"environment":{"type":"object","description":"Environmental sensor data","properties":{"pressure_hPa":{"type":"number","description":"Atmospheric pressure in hPa","example":1013.2},"temperature_C":{"type":"number","description":"Temperature in Celsius","example":25.4}}},"deviceStatus":{"type":"object","description":"Device status information","properties":{"batteryLevel":{"type":"number","description":"Battery level percentage","example":91},"bleConnection":{"type":"string","description":"Bluetooth connection status","example":"connected","enum":["connected","disconnected","connecting"]}}},"items":{"type":"array","description":"Legacy format: Array of telemetry items (optional, for backward compatibility)","items":{"type":"object","properties":{"timestamp":{"type":"string","format":"date-time"},"ppg":{"$ref":"#/components/schemas/SyncRequest/properties/ppg"},"bloodPressure":{"$ref":"#/components/schemas/SyncRequest/properties/bloodPressure"},"imu":{"$ref":"#/components/schemas/SyncRequest/properties/imu"},"environment":{"$ref":"#/components/schemas/SyncRequest/properties/environment"},"deviceStatus":{"$ref":"#/components/schemas/SyncRequest/properties/deviceStatus"}}}}}},"DeviceSyncRequest":{"type":"object","description":"Legacy device telemetry sync request (for backwards compatibility)","allOf":[{"$ref":"#/components/schemas/SyncRequest"}]},"FitnessTrackerData":{"type":"object","description":"Complete daily fitness summary with time-series metrics, workouts, and sleep data. Idempotent by user + device + date. Note: user_id is extracted from JWT token.","required":["device_id","date"],"properties":{"device_id":{"type":"string","description":"Unique device/smartwatch identifier (e.g., watch serial number, IMEI, or custom ID)","example":"WATCH-ABC-12345"},"date":{"type":"string","format":"date","description":"Date for this daily summary (YYYY-MM-DD)","example":"2026-01-29"},"time_series_metrics":{"type":"array","description":"Array of time-series health metrics throughout the day","items":{"type":"object","required":["timestamp"],"properties":{"timestamp":{"type":"string","format":"date-time","description":"Timestamp of the metric reading","example":"2026-01-29T08:00:00Z"},"heart_rate":{"type":"integer","description":"Heart rate in BPM","example":72},"steps":{"type":"integer","description":"Step delta for this timestamp","example":320},"hrv":{"type":"integer","description":"Heart Rate Variability in milliseconds","example":45},"respiratory_rate":{"type":"number","description":"Breaths per minute","example":16.5},"spo2":{"type":"integer","description":"Blood oxygen saturation percentage (0-100)","minimum":0,"maximum":100,"example":98}}}},"daily_cumulatives":{"type":"object","description":"Cumulative daily metrics","properties":{"steps":{"type":"integer","description":"Total steps taken","minimum":0,"example":10524},"total_calories_burned":{"type":"integer","description":"Total calories burned (kcal)","example":2340},"active_calories_burned":{"type":"integer","description":"Active calories burned during workouts (kcal)","example":680},"distance_meters":{"type":"number","description":"Total distance traveled in meters","example":8420.5},"floors_climbed":{"type":"integer","description":"Number of floors climbed","example":12}}},"detected_workouts":{"type":"array","description":"Array of detected workout sessions","items":{"type":"object","required":["workout_type","start_time","end_time"],"properties":{"workout_type":{"type":"string","description":"Type of workout","enum":["run","walk","cycling","swimming","hiit","other"],"example":"run"},"start_time":{"type":"string","format":"date-time","description":"Workout start time","example":"2026-01-29T07:00:00Z"},"end_time":{"type":"string","format":"date-time","description":"Workout end time","example":"2026-01-29T07:45:00Z"},"average_heart_rate":{"type":"integer","description":"Average heart rate during workout (BPM)","example":145},"calories_burned":{"type":"integer","description":"Calories burned during workout (kcal)","example":420},"intensity_score":{"type":"number","description":"Workout intensity score (0-10)","example":8.5}}}},"sleep_data":{"type":"object","description":"Sleep summary for the night/day","properties":{"total_sleep_duration_minutes":{"type":"integer","description":"Total sleep duration in minutes","example":456},"sleep_score":{"type":"integer","description":"Overall sleep quality score (0-100)","minimum":0,"maximum":100,"example":85},"stages":{"type":"object","description":"Sleep stage breakdown","properties":{"rem_minutes":{"type":"integer","description":"REM sleep duration in minutes","example":98},"deep_minutes":{"type":"integer","description":"Deep sleep duration in minutes","example":112},"light_minutes":{"type":"integer","description":"Light sleep duration in minutes","example":230},"awake_minutes":{"type":"integer","description":"Time awake in minutes","example":16}}},"bedtime":{"type":"string","format":"date-time","description":"Time went to bed","example":"2026-01-28T22:30:00Z"},"wakeup_time":{"type":"string","format":"date-time","description":"Time woke up","example":"2026-01-29T06:06:00Z"}}}}},"SyncResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"processed":{"type":"number","example":5},"message":{"type":"string","example":"Batch processed successfully"}}},"AISuggestRequest":{"type":"object","required":["deviceId"],"properties":{"userId":{"type":"string","description":"Optional user identifier","example":"user-123"},"deviceId":{"type":"string","description":"Device identifier","example":"device-123"},"type":{"type":"string","description":"Type of AI suggestion","example":"daily_summary","default":"daily_summary"}}},"AISuggestResponse":{"type":"object","properties":{"suggestion":{"type":"string","example":"Based on your activity today, consider taking a 10-minute walk to reach your daily goal."},"confidence":{"type":"number","example":0.85},"type":{"type":"string","example":"daily_summary"}}},"Device":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"device_id":{"type":"string"},"user_id":{"type":"string","format":"uuid"},"device_name":{"type":"string"},"device_type":{"type":"string","enum":["smartwatch","fitness_tracker","smartphone","other"]},"model":{"type":"string"},"firmware_version":{"type":"string"},"battery_level":{"type":"number","minimum":0,"maximum":100},"last_sync_at":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["active","inactive","offline"]},"settings":{"type":"object"},"created_at":{"type":"string","format":"date-time"}}},"FeatureRequest":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"title":{"type":"string","maxLength":255},"description":{"type":"string"},"category":{"type":"string","enum":["ui_ux","performance","fitness_tracking","health_metrics","notifications","device_sync","data_export","social","ai_insights","integrations","other"]},"priority":{"type":"string","enum":["low","medium","high","critical"]},"status":{"type":"string","enum":["open","under_review","planned","in_progress","completed","closed","wont_fix"]},"votes_count":{"type":"integer"},"comments_count":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"BugReport":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"title":{"type":"string"},"description":{"type":"string"},"severity":{"type":"string","enum":["low","medium","high","critical"]},"category":{"type":"string"},"device_model":{"type":"string"},"app_version":{"type":"string"},"status":{"type":"string","enum":["open","under_investigation","reproduced","in_progress","fixed","closed","cannot_reproduce","not_a_bug"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"HealthGoal":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"goal_type":{"type":"string","enum":["daily_steps","weekly_exercise","weight_target","sleep_hours","water_intake","calorie_burn","heart_rate_zone"]},"target_value":{"type":"number"},"target_unit":{"type":"string"},"current_value":{"type":"number"},"progress_percentage":{"type":"number"},"target_date":{"type":"string","format":"date"},"status":{"type":"string","enum":["active","completed","paused","cancelled"]},"reminder_enabled":{"type":"boolean"},"reminder_time":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"FoodItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"brand":{"type":"string"},"category":{"type":"string","enum":["breakfast","lunch","dinner","snack","beverage"]},"food_type":{"type":"string"},"serving_size_g":{"type":"number"},"serving_size_unit":{"type":"string"},"calories":{"type":"number"},"protein_g":{"type":"number"},"carbs_g":{"type":"number"},"fat_g":{"type":"number"},"saturated_fat_g":{"type":"number","nullable":true,"description":"Null when the source has no saturated fat"},"fiber_g":{"type":"number"},"sugar_g":{"type":"number"},"ingredients":{"type":"array","items":{"type":"string"}},"prep_time_minutes":{"type":"number"},"image_url":{"type":"string"},"verified":{"type":"boolean"}}},"Meal":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"meal_type":{"type":"string","enum":["breakfast","lunch","dinner","snack"]},"meal_name":{"type":"string"},"meal_date":{"type":"string","format":"date"},"meal_time":{"type":"string","format":"time"},"total_calories":{"type":"number"},"total_protein_g":{"type":"number"},"total_carbs_g":{"type":"number"},"total_fat_g":{"type":"number"},"items":{"type":"array","items":{"type":"object"}}}},"DailyNutrition":{"type":"object","properties":{"summary_date":{"type":"string","format":"date"},"total_calories":{"type":"number"},"total_protein_g":{"type":"number"},"total_carbs_g":{"type":"number"},"total_fat_g":{"type":"number"},"total_water_ml":{"type":"number"},"breakfast_count":{"type":"number"},"lunch_count":{"type":"number"},"dinner_count":{"type":"number"},"snack_count":{"type":"number"}}},"Notification":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"title":{"type":"string"},"message":{"type":"string"},"type":{"type":"string","enum":["health_alert","goal_reminder","achievement","social","system","marketing"]},"priority":{"type":"string","enum":["low","medium","high","urgent"]},"status":{"type":"string","enum":["pending","sending","sent","delivered","read","failed","cancelled","expired","suppressed","skipped"]},"deep_link":{"type":"string","nullable":true,"description":"App route to open on tap, e.g. hiyd://recommendation"},"scheduled_at":{"type":"string","format":"date-time"},"sent_at":{"type":"string","format":"date-time"},"read_at":{"type":"string","format":"date-time"},"metadata":{"type":"object"},"created_at":{"type":"string","format":"date-time"}}},"OnboardingQuestion":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"question_text":{"type":"string"},"subtitle":{"type":"string","nullable":true},"question_order":{"type":"integer"},"is_multiple_choice":{"type":"boolean"},"input_type":{"type":"string","enum":["choice","grid","skin","text"]},"category":{"type":"string"},"answer_options":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"option_text":{"type":"string"},"option_order":{"type":"integer"},"option_value":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"icon":{"type":"string","nullable":true},"swatch_hex":{"type":"string","nullable":true}}}}}},"SubscriptionPlan":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"display_name":{"type":"string"},"description":{"type":"string"},"price":{"type":"number"},"currency":{"type":"string"},"interval":{"type":"string","enum":["month","year","lifetime","free"]},"interval_count":{"type":"integer"},"trial_period_days":{"type":"integer"},"features":{"type":"array","items":{"type":"string"}}}},"UserSubscription":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"plan_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["active","trialing","past_due","canceled","expired"]},"current_period_start":{"type":"string","format":"date-time"},"current_period_end":{"type":"string","format":"date-time"},"trial_start":{"type":"string","format":"date-time"},"trial_end":{"type":"string","format":"date-time"}}},"User":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"firebase_uid":{"type":"string"},"email":{"type":"string","format":"email"},"first_name":{"type":"string","maxLength":50},"last_name":{"type":"string","maxLength":50},"display_name":{"type":"string","description":"Legacy full name, kept in sync server-side (trim(first_name || ' ' || last_name))"},"phone_number":{"type":"string"},"profile_image_url":{"type":"string"},"date_of_birth":{"type":"string","format":"date"},"gender":{"type":"string","enum":["male","female","non_binary","other","prefer_not_to_say"]},"height_cm":{"type":"number"},"weight_kg":{"type":"number"},"target_weight_kg":{"type":"number"},"activity_level":{"type":"string","enum":["sedentary","light","moderate","active","very_active"]},"fitness_goals":{"type":"array","items":{"type":"string"}},"daily_calorie_target":{"type":"number"},"onboarding_completed":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"last_active_at":{"type":"string","format":"date-time"}}}},"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Enter your JWT token (Backend JWT issued after login)"},"FirebaseAuth":{"type":"http","scheme":"bearer","bearerFormat":"Firebase ID Token","description":"Enter your Firebase ID token (from Firebase Authentication)"}}},"security":[{"BearerAuth":[]},{"FirebaseAuth":[]}],"paths":{"/api/v1/ai/suggest":{"post":{"summary":"Get AI-powered suggestions","description":"Generates AI suggestions based on user's smartwatch data.","tags":["AI"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AISuggestRequest"}}}},"responses":{"200":{"description":"AI suggestion generated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AISuggestResponse"}}}},"400":{"description":"Bad request - deviceId is required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"deviceId required"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/action":{"get":{"summary":"Hosted email-action page (verify email, reset password)","description":"Landing page for the action links in hiyd's verification emails; `mode` picks the page. On mobile devices with the hiyd app installed the same URL can open as a Universal Link / App Link instead. New password-reset links go to /auth/reset-password, but mode=resetPassword is still served here for links sent before that change. Modes without a hosted page (e.g. recoverEmail) are redirected to the default Firebase handler.\n","tags":["Auth Pages"],"parameters":[{"in":"query","name":"mode","schema":{"type":"string"},"description":"Firebase action mode (verifyEmail, resetPassword, ...)"},{"in":"query","name":"oobCode","schema":{"type":"string"},"description":"Firebase out-of-band action code"}],"responses":{"200":{"description":"HTML page for the requested mode","content":{"text/html":{"schema":{"type":"string"}}}},"302":{"description":"Mode not hosted here; redirected to Firebase's handler"}}}},"/auth/reset-password":{"get":{"summary":"Hosted password-reset page","description":"Landing page for the link in hiyd's password-reset email. It has its own path, rather than /auth/action, because Android App Links can't filter on the query string: keeping reset links off /auth/action lets the app claim that path for verification links while reset links stay in the browser. A link whose `mode` is something other than resetPassword is redirected to /auth/action with its query intact.\n","tags":["Auth Pages"],"parameters":[{"in":"query","name":"mode","schema":{"type":"string"},"description":"Firebase action mode; expected to be resetPassword"},{"in":"query","name":"oobCode","schema":{"type":"string"},"description":"Firebase out-of-band action code"}],"responses":{"200":{"description":"The password-reset page","content":{"text/html":{"schema":{"type":"string"}}}},"302":{"description":"Not a reset link; redirected to /auth/action"}}}},"/auth/action/apply":{"post":{"summary":"Apply a Firebase email-verification code","description":"Used by the hosted verification page to mark the email as verified.","tags":["Auth Pages"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["oobCode"],"properties":{"oobCode":{"type":"string","description":"Out-of-band code from the verification link"}}}}}},"responses":{"200":{"description":"Email verified","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"email":{"type":"string"}}}}}},"400":{"description":"Invalid, expired, or already-used code"},"429":{"description":"Rate limit exceeded"}}}},"/auth/action/verify-reset":{"post":{"summary":"Check a password-reset code without using it","description":"Used by the hosted reset page to show whose password is being reset.","tags":["Auth Pages"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["oobCode"],"properties":{"oobCode":{"type":"string"}}}}}},"responses":{"200":{"description":"Code is valid; returns the account's email"},"400":{"description":"Invalid, expired, or already-used code"},"429":{"description":"Rate limit exceeded"}}}},"/auth/action/reset-password":{"post":{"summary":"Set a new password with a reset code","description":"Used by the hosted reset page. Consumes the code.","tags":["Auth Pages"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["oobCode","newPassword"],"properties":{"oobCode":{"type":"string"},"newPassword":{"type":"string"}}}}}},"responses":{"200":{"description":"Password updated"},"400":{"description":"Invalid/expired code, or the password was rejected (weak_password)"},"429":{"description":"Rate limit exceeded"}}}},"/api/devices":{"get":{"summary":"Get user's devices","tags":["Devices"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["active","inactive","offline"]},"description":"Filter devices by status"}],"responses":{"200":{"description":"Devices retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"devices":{"type":"array","items":{"$ref":"#/components/schemas/Device"}}}}}}}}},"post":{"summary":"Register a new device","tags":["Devices"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["device_id","device_name","device_type"],"properties":{"device_id":{"type":"string"},"device_name":{"type":"string"},"device_type":{"type":"string","enum":["smartwatch","fitness_tracker","smartphone","other"]},"model":{"type":"string"},"firmware_version":{"type":"string"},"battery_level":{"type":"number","minimum":0,"maximum":100}}}}}},"responses":{"201":{"description":"Device registered successfully","content":{"application/json":{"schema":{"type":"object","properties":{"device":{"$ref":"#/components/schemas/Device"}}}}}},"409":{"description":"Device already exists"}}}},"/api/devices/{deviceId}":{"delete":{"summary":"Unregister device","tags":["Devices"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"deviceId","required":true,"schema":{"type":"string"},"description":"Device ID"}],"responses":{"200":{"description":"Device unregistered successfully"},"404":{"description":"Device not found"}}}},"/api/feature-flags":{"get":{"summary":"Feature flags for the mobile app","description":"Public. Returns every server-side feature flag as true, false or null. null means \"no server opinion\": the app keeps its built-in default for its flavor. When the request carries a valid backend session JWT, that user's per-user overrides are merged in; a missing, invalid or expired token returns the global values (never 401). Keys the app does not know should be ignored, and missing keys treated as null.\n","tags":["Feature Flags"],"security":[],"parameters":[{"in":"header","name":"Authorization","required":false,"schema":{"type":"string"},"description":"Optional \"Bearer <backend JWT>\" to include the user's overrides"}],"responses":{"200":{"description":"Flag values","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"flags":{"type":"object","additionalProperties":{"type":"boolean","nullable":true},"example":{"coach":true,"nutrition":null,"sleep":false}}}}}}},"503":{"description":"Flags could not be read; the app keeps its cached values","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"flags_unavailable"},"message":{"type":"string"}}}}}}}}},"/api/feedback/features":{"post":{"summary":"Submit a feature request","description":"Users can request new features or improvements for the smartwatch app","tags":["Feedback"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","description","category"],"properties":{"title":{"type":"string","maxLength":255,"example":"Add dark mode to app"},"description":{"type":"string","maxLength":5000,"example":"It would be great to have a dark mode option..."},"category":{"type":"string","example":"ui_ux"},"priority":{"type":"string","enum":["low","medium","high","critical"],"default":"medium"}}}}}},"responses":{"201":{"description":"Feature request created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"feature_request":{"$ref":"#/components/schemas/FeatureRequest"}}}}}},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"}}},"get":{"summary":"List all feature requests","description":"Get paginated list of feature requests with optional filtering and sorting","tags":["Feedback"],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["open","under_review","planned","in_progress","completed","closed","wont_fix"]},"description":"Filter by status"},{"in":"query","name":"category","schema":{"type":"string"},"description":"Filter by category"},{"in":"query","name":"sort_by","schema":{"type":"string","enum":["newest","votes","comments"],"default":"newest"}},{"in":"query","name":"page","schema":{"type":"integer","default":1}},{"in":"query","name":"limit","schema":{"type":"integer","default":20,"maximum":100}}],"responses":{"200":{"description":"List of feature requests","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/FeatureRequest"}},"pagination":{"type":"object","properties":{"total":{"type":"integer"},"page":{"type":"integer"},"limit":{"type":"integer"},"pages":{"type":"integer"}}}}}}}}}}},"/api/feedback/features/{id}":{"get":{"summary":"Get feature request details","tags":["Feedback"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Feature request details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureRequest"}}}},"404":{"description":"Feature request not found"}}}},"/api/feedback/features/{id}/vote":{"post":{"summary":"Vote on a feature request","description":"Users can upvote feature requests they want to see implemented","tags":["Feedback"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Vote registered"},"400":{"description":"User already voted on this feature"},"401":{"description":"Unauthorized"},"404":{"description":"Feature request not found"}}},"delete":{"summary":"Remove vote from a feature request","tags":["Feedback"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Vote removed"},"401":{"description":"Unauthorized"}}}},"/api/feedback/bugs":{"post":{"summary":"Submit a bug report","description":"Report bugs found in the smartwatch app with detailed reproduction steps","tags":["Feedback"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","description","severity","category"],"properties":{"title":{"type":"string","maxLength":255,"example":"App crashes when viewing weekly stats"},"description":{"type":"string","maxLength":5000},"severity":{"type":"string","enum":["low","medium","high","critical"]},"category":{"type":"string","enum":["crash","performance","data_loss","sync_issue","ui_glitch","calculation_error","notification","device_connectivity","security","other"]},"device_model":{"type":"string"},"app_version":{"type":"string"},"os_version":{"type":"string"},"steps_to_reproduce":{"type":"string"},"expected_behavior":{"type":"string"},"actual_behavior":{"type":"string"}}}}}},"responses":{"201":{"description":"Bug report submitted successfully"},"400":{"description":"Validation error"},"401":{"description":"Unauthorized"}}},"get":{"summary":"List all bug reports","description":"Get paginated list of bug reports with filtering and sorting options","tags":["Feedback"],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["open","under_investigation","reproduced","in_progress","fixed","closed","cannot_reproduce","not_a_bug"]}},{"in":"query","name":"severity","schema":{"type":"string","enum":["low","medium","high","critical"]}},{"in":"query","name":"category","schema":{"type":"string"}},{"in":"query","name":"sort_by","schema":{"type":"string","enum":["newest","severity","comments"],"default":"newest"}},{"in":"query","name":"page","schema":{"type":"integer","default":1}},{"in":"query","name":"limit","schema":{"type":"integer","default":20,"maximum":100}}],"responses":{"200":{"description":"List of bug reports","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/BugReport"}},"pagination":{"type":"object"}}}}}}}}},"/api/feedback/bugs/{id}":{"get":{"summary":"Get bug report details","tags":["Feedback"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Bug report details"},"404":{"description":"Bug report not found"}}}},"/api/feedback/features/{id}/comments":{"post":{"summary":"Add comment to feature request","tags":["Feedback"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["comment"],"properties":{"comment":{"type":"string","maxLength":2000}}}}}},"responses":{"201":{"description":"Comment added"},"401":{"description":"Unauthorized"},"404":{"description":"Feature request not found"}}},"get":{"summary":"Get comments on feature request","tags":["Feedback"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"query","name":"page","schema":{"type":"integer","default":1}},{"in":"query","name":"limit","schema":{"type":"integer","default":20}}]}},"/api/feedback/bugs/{id}/comments":{"post":{"summary":"Add comment to bug report","tags":["Feedback"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["comment"],"properties":{"comment":{"type":"string","maxLength":2000}}}}}},"responses":{"201":{"description":"Comment added"}}},"get":{"summary":"Get comments on bug report","tags":["Feedback"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}]}},"/api/goals":{"get":{"summary":"Get user's health goals","tags":["Goals"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["active","completed","paused","cancelled","all"],"default":"active"},"description":"Filter goals by status"},{"in":"query","name":"goal_type","schema":{"type":"string","enum":["daily_steps","weekly_exercise","weight_target","sleep_hours","water_intake","calorie_burn","heart_rate_zone"]},"description":"Filter by goal type"}],"responses":{"200":{"description":"Goals retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"goals":{"type":"array","items":{"$ref":"#/components/schemas/HealthGoal"}}}}}}}}},"post":{"summary":"Create a new health goal","tags":["Goals"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["goal_type","target_value","target_unit"],"properties":{"goal_type":{"type":"string","enum":["daily_steps","weekly_exercise","weight_target","sleep_hours","water_intake","calorie_burn","heart_rate_zone"]},"target_value":{"type":"number","minimum":0},"target_unit":{"type":"string"},"target_date":{"type":"string","format":"date"},"reminder_enabled":{"type":"boolean","default":true},"reminder_time":{"type":"string","pattern":"^([01]?[0-9]|2[0-3]):[0-5][0-9]$"}}}}}},"responses":{"201":{"description":"Goal created successfully"}}}},"/api/goals/{goalId}":{"get":{"summary":"Get specific goal details","tags":["Goals"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"goalId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Goal details retrieved successfully"}}},"put":{"summary":"Update goal","tags":["Goals"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"goalId","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"target_value":{"type":"number"},"target_date":{"type":"string","format":"date"},"reminder_enabled":{"type":"boolean"},"reminder_time":{"type":"string"},"status":{"type":"string","enum":["active","completed","paused","cancelled"]}}}}}},"responses":{"200":{"description":"Goal updated successfully"}}},"delete":{"summary":"Delete goal","tags":["Goals"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"goalId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Goal deleted successfully"}}}},"/api/goals/templates":{"get":{"summary":"Get goal templates","tags":["Goals"],"security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Goal templates retrieved successfully"}}}},"/api/goals/progress/summary":{"get":{"summary":"Get goals progress summary","tags":["Goals"],"security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Goals progress summary retrieved successfully"}}}},"/api/health/move-summary":{"get":{"summary":"Get comprehensive health summary (Pinned Stats)","description":"Returns all pinned stats including movement, recovery, heart rate, sleep, water intake, stress level, calories, and distance for the day","tags":["Health"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"date","schema":{"type":"string","format":"date","example":"2026-01-24"},"description":"Date for the summary (defaults to today)"},{"in":"query","name":"time_zone","schema":{"type":"string","example":"America/Los_Angeles"},"description":"IANA zone the day and its hours are counted in. Omitted or\nunrecognised values mean UTC. steps.total is the local day's\ntotal: phone (Health) steps when there are any, else the\ntracker's hourly rows in this zone (the UTC-day rollup only on a\nday the zone sits at UTC+0 all day). No tracker hours yet means\n0 with an empty chart, never last evening's steps.\n"}],"responses":{"200":{"description":"Comprehensive health summary data","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string","format":"date"},"movement_score":{"type":"object","properties":{"value":{"type":"number","example":75},"rating":{"type":"string","example":"Excellent"},"steps":{"type":"number"},"components":{"type":"object","description":"Weighted Move Score inputs and normalized component scores"}}},"recovery_score":{"type":"object","properties":{"value":{"type":"number","example":74},"rating":{"type":"string","example":"Need Work"}}},"heart_rate":{"type":"object","properties":{"current":{"type":"number","example":72},"average":{"type":"number"},"min":{"type":"number"},"max":{"type":"number"},"zones":{"type":"object","description":"Minutes per heart-rate zone for the selected day. Max HR is the age formula (208 - 0.7 x age) or, once 3+ workouts in 90 days confirm it, the user's own (max_heart_rate_source = age_formula | learned). Zones are Karvonen on the overnight resting HR (method = karvonen), or percent of max when there is none (method = percent_of_max). Same zones the engine's Move score is built on.\n"},"chart_data":{"type":"array","items":{"type":"object","properties":{"timestamp":{"type":"string"},"value":{"type":"number"}}}}}},"steps":{"type":"object","properties":{"total":{"type":"number","example":5120},"distance_km":{"type":"number"},"chart_data":{"type":"array"}}},"strength_time":{"type":"object","description":"Strength workout time over the last 7 days ending on the selected date","properties":{"period":{"type":"string","example":"last_7_days"},"total_minutes":{"type":"number","example":90},"formatted":{"type":"string","example":"1h 30m"},"workout_count":{"type":"number"}}},"sleep":{"type":"object","properties":{"total_minutes":{"type":"number","example":372},"hours":{"type":"number","example":6},"minutes":{"type":"number","example":12},"formatted":{"type":"string","example":"6h 12m"},"deep_sleep_minutes":{"type":"number"},"rem_sleep_minutes":{"type":"number"},"avg_heart_rate":{"type":"number"},"lowest_heart_rate":{"type":"number"}}},"sleep_score":{"type":"object","properties":{"value":{"type":"number","example":82},"percentage":{"type":"number","example":82},"rating":{"type":"string","example":"Excellent"}}},"water_intake":{"type":"object","properties":{"total_ml":{"type":"number","example":500},"glasses":{"type":"number","example":2},"formatted":{"type":"string","example":"2 Glasses"}}},"stress":{"type":"object","properties":{"level":{"type":"string","example":"LOW","enum":["UNKNOWN","LOW","MODERATE","HIGH","VERY HIGH"]},"hrv_score":{"type":"number","example":65},"source":{"type":"string","example":"HRV"}}},"calories":{"type":"object","properties":{"total":{"type":"number","example":2340},"active":{"type":"number","example":450}}},"active_minutes":{"type":"object","properties":{"total":{"type":"number","example":87},"formatted":{"type":"string","example":"87 mins"}}},"distance":{"type":"object","properties":{"meters":{"type":"number","example":3800},"km":{"type":"string","example":"3.80"},"formatted":{"type":"string","example":"3.8 km"}}},"daily_summary":{"type":"object","properties":{"calories_burned":{"type":"number"},"active_calories":{"type":"number"},"floors_climbed":{"type":"number"},"active_minutes":{"type":"number"},"sedentary_minutes":{"type":"number"}}}}}}}}}}},"/api/health/move-summary/all":{"get":{"summary":"Get move summary data for all periods","description":"Returns move summary charts and totals for day, week, month, 6m, and year.\nEvery period counts LOCAL calendar days in time_zone. A tracker day's\ntotal comes from its hourly rows in that zone; the UTC-day rollup is\nused only on a day the zone sits at UTC+0 all day. A day without\ntracker hours has no tracker steps (the Day total is 0 with an empty\nchart). Phone (Health) steps win a day whenever they exist.\n","tags":["Health"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"date","schema":{"type":"string","format":"date","example":"2026-01-24"},"description":"Date for the summary (defaults to today)"},{"in":"query","name":"period","schema":{"type":"string","enum":["day","week","month","6m","year"]},"description":"Optional single period to return; omit to get all periods"},{"in":"query","name":"time_zone","schema":{"type":"string","example":"America/Los_Angeles"},"description":"IANA zone the days and hours are counted in. Omitted or unrecognised values mean UTC."},{"in":"query","name":"window","schema":{"type":"string","enum":["calendar","trailing"],"default":"calendar"},"description":"trailing: Week is the 7 local days ending on date (inclusive) and\nMonth the 30 local days ending on it, with steps null on days\nwithout steps. calendar (or omitted): the calendar week (Sunday\nto Saturday) and month holding date, with 0 on days without\nsteps. Day, 6m and year are always calendar.\n"}],"responses":{"200":{"description":"Move summary data per period. Each summaries.<period> holds:\ndate; window ('trailing' when a trailing window was applied, else\n'calendar'); date_range {start, end} (inclusive, local dates);\nsteps.total; steps.average_daily_steps, the mean over the days in\nthe range that have steps, leaving out today (still in progress)\nunless it is the only such day; steps.average_days, how many days\nthat mean covers; steps.chart_data (day: {time, steps} per hour\nwith steps; week/month: {date, label, steps} for every day of\nthe range; 6m/year: {period, label, steps} per month).\n","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string"},"summaries":{"type":"object","additionalProperties":{"type":"object","properties":{"date":{"type":"string","format":"date"},"window":{"type":"string","enum":["calendar","trailing"]},"date_range":{"type":"object","properties":{"start":{"type":"string","format":"date"},"end":{"type":"string","format":"date"}}},"steps":{"type":"object","properties":{"total":{"type":"integer"},"average_daily_steps":{"type":"integer"},"average_days":{"type":"integer"},"chart_data":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","format":"date"},"label":{"type":"string"},"time":{"type":"string"},"period":{"type":"string"},"steps":{"type":"integer","nullable":true}}}}}}}}}}}}}},"400":{"description":"Malformed date or unknown period"}}}},"/api/health/rest-summary":{"get":{"summary":"Get rest/recovery summary with sleep and stress data","description":"Returns sleep score, deep rest duration, and stress levels","tags":["Health"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"date","schema":{"type":"string","format":"date","example":"2026-01-24"},"description":"Date for the summary (defaults to today)"},{"in":"query","name":"time_zone","schema":{"type":"string","example":"America/Los_Angeles"},"description":"IANA zone the client lives in. Demo sleep rows are attributed to the\nwake day in this zone. Band nights need no zone. Omitted or\nunrecognised values mean UTC.\n"}],"responses":{"200":{"description":"Rest summary data. sleep.source is 'band', 'demo' or null.\nskin_temp holds last night's wrist skin temperature (°C, sleep\nwindow only), the median of the previous 14 nights as baseline\n(needs 3 nights) and the deviation between them; null while there\nis not enough data.\n"},"400":{"description":"Malformed date"}}}},"/api/health/current-stats":{"get":{"summary":"Get current real-time health stats (Pinned Stats)","description":"Returns all pinned stats including steps, calories, heart rate, sleep, water, stress, and active minutes","tags":["Health"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"date","schema":{"type":"string","format":"date","example":"2026-01-24"},"description":"Day to describe (defaults to today). Latest-reading fallbacks never look past the end of this day."},{"in":"query","name":"time_zone","schema":{"type":"string","example":"America/Los_Angeles"},"description":"IANA zone the day is measured in: readings, resting HR (00:00-08:59),\nworkouts and demo sleep are all attributed to `date` on this clock.\nBand nights need no zone. Omitted or unrecognised values mean UTC.\nsteps_today.total is the local day's total, the same number as\n/move-summary steps.total: tracker steps come from the hourly rows\nin this zone (the UTC-day rollup only on a day the zone sits at\nUTC+0 all day), and no tracker hours means 0.\n"}],"responses":{"200":{"description":"Current stats with all pinned metrics"}}}},"/api/health/data-days":{"get":{"summary":"Days in a range that have scoreable health data","description":"Lists the calendar days between start and end (inclusive) on which the\nuser has at least one of: a sleep night, HRV readings, heart-rate readings,\nor a workout — from Apple Health / demo data (fitness_*) or the band\n(metrics_sink). Steps alone do not count, nor does an Apple Health /\nHealth Connect sleep night (sleep source policy: band and demo only).\nhiyd-ai-engine uses this to find days it has data for but no score yet.\nDay attribution matches /rest-summary and /current-stats (sleep by wake\nday, readings by date) in the zone given by time_zone, or UTC days\nwhen it is omitted. Band sleep nights keep their own label (night_date\n+ 1), whatever the zone.\n","tags":["Health"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"start","required":true,"schema":{"type":"string","format":"date"}},{"in":"query","name":"end","required":true,"schema":{"type":"string","format":"date"}},{"in":"query","name":"time_zone","required":false,"description":"IANA zone the days are counted in (e.g. America/Los_Angeles). Unknown or missing means UTC.","schema":{"type":"string"}}],"responses":{"200":{"description":"Sorted list of days (YYYY-MM-DD) with data"},"400":{"description":"Missing/invalid dates, end before start, or range wider than 92 days"}}}},"/api/health/heart-rate-timeline":{"get":{"summary":"Get heart rate timeline for the day","description":"Returns heart rate data points throughout the day with hourly granularity","tags":["Health"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"date","schema":{"type":"string","format":"date","example":"2026-01-24"},"description":"Date for the timeline (defaults to today)"},{"in":"query","name":"granularity","schema":{"type":"string","enum":["hourly","week","month","year"],"default":"hourly"},"description":"- hourly: Hourly data for specified date\n- week: Daily data for last 7 days\n- month: Daily data for last 30 days\n- year: Monthly data for last 12 months\n"},{"in":"query","name":"time_zone","schema":{"type":"string","example":"America/Los_Angeles"},"description":"IANA zone the day boundary and the hourly buckets (hour_24) are\nresolved in. Omitted or unrecognised values mean UTC. For many days\nof the hourly view's daytime low and stats.current at once, see\n/api/health/heart-rate-days.\n"}],"responses":{"200":{"description":"Heart rate timeline data. Hourly stats carry current, the day's\nlatest heart-rate reading, and current_at, the ISO-8601 UTC time\nof that reading (the phone row's time, or for a tracker reading\nits window's midpoint), both null when the day has none. Week,\nmonth and year stats have neither.\n"}}}},"/api/health/heart-rate-days":{"get":{"summary":"Each day's daytime low and last heart-rate reading over a range","description":"One entry for every date from start to end inclusive, in order, so\nthe app can load many days of resting heart rate in one request\ninstead of one hourly /heart-rate-timeline per day. Each day is\nsummarized exactly as /heart-rate-timeline?granularity=hourly&date=D\nwith the same time_zone summarizes it:\n- daytime_low: the lowest hourly point between 11 AM and 11 PM\n  (hour_24 11 to 22) in that response, taking each hour's min (its\n  heart_rate when min is missing) and skipping missing or\n  non-positive values; null when there is none.\n- last_reading: that response's stats.current, the day's latest\n  heart-rate reading; null when the day has none.\n- last_reading_at: that response's stats.current_at, the ISO-8601\n  UTC time of last_reading; null when last_reading is null.\n","tags":["Health"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"start","required":true,"schema":{"type":"string","format":"date"}},{"in":"query","name":"end","required":true,"schema":{"type":"string","format":"date"}},{"in":"query","name":"time_zone","schema":{"type":"string","example":"America/Los_Angeles"},"description":"IANA zone the days and hours are counted in. Omitted or unrecognised values mean UTC."}],"responses":{"200":{"description":"One entry per date from start to end","content":{"application/json":{"schema":{"type":"object","properties":{"days":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","format":"date"},"daytime_low":{"type":"number","nullable":true},"last_reading":{"type":"number","nullable":true},"last_reading_at":{"type":"string","format":"date-time","nullable":true}}}}}}}}},"400":{"description":"Missing or malformed start/end (invalid_date), end before start (invalid_range), or more than 62 days (range_too_wide)"}}}},"/api/health/sleep-chart":{"get":{"summary":"Get sleep chart data with heart rate overlay","description":"Returns sleep session data with sleep stages timeline and heart rate data for visualization","tags":["Health"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"date","schema":{"type":"string","format":"date"},"description":"Date of sleep session (defaults to last night)"},{"in":"query","name":"granularity","schema":{"type":"string","enum":["day","week","month"],"default":"day"},"description":"- day: Single sleep session with stages and heart rate\n- week: Daily sleep data for last 7 days\n- month: Daily sleep data for last 30 days\n"},{"in":"query","name":"start","schema":{"type":"string","format":"date"},"description":"week/month only: first wake day of the window, so the window is\n[start, date] instead of the trailing 7 / 30 days. Ignored (trailing\nwindow used) unless start <= date and the span is at most 42 days.\n"},{"in":"query","name":"time_zone","schema":{"type":"string","example":"America/Los_Angeles"},"description":"IANA zone the client lives in. Demo sleep rows are attributed to the\nwake day in this zone. Band nights need no zone. Omitted or\nunrecognised values mean UTC.\n"}],"responses":{"200":{"description":"Sleep chart data retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string","format":"date"},"sleep_session":{"type":"object","properties":{"bedtime":{"type":"string","format":"date-time"},"wakeup_time":{"type":"string","format":"date-time"},"total_duration":{"type":"string"},"total_duration_minutes":{"type":"number"},"sleep_score":{"type":"number"}}},"sleep_stages":{"type":"array","items":{"type":"object","properties":{"stage":{"type":"string"},"start_time":{"type":"string"},"end_time":{"type":"string"},"duration_minutes":{"type":"number"}}}},"heart_rate_data":{"type":"array","items":{"type":"object","properties":{"timestamp":{"type":"string"},"heart_rate":{"type":"number"}}}},"summary":{"type":"object","properties":{"time_in_bed":{"type":"string"},"time_asleep":{"type":"string"},"deep_sleep":{"type":"string"},"light_sleep":{"type":"string"},"rem_sleep":{"type":"string"},"awake":{"type":"string"},"avg_heart_rate":{"type":"number"},"lowest_heart_rate":{"type":"number"}}},"source":{"type":"string","enum":["band","demo"]},"stages_source":{"type":"string","enum":["band","demo","none"],"description":"none = a band night with no stored stage timeline"},"stage_timeline":{"type":"array","description":"Stage segments sorted by start_time, lowercase stage names\n(awake, light, deep, rem), UTC instants ending in Z. For a\nband night the legacy sleep_stages field is [] and this one\ncarries the timeline.\n","items":{"type":"object","properties":{"stage":{"type":"string"},"start_time":{"type":"string","format":"date-time"},"end_time":{"type":"string","format":"date-time"},"duration_minutes":{"type":"number"}}}},"bedtime":{"type":"string","format":"date-time","nullable":true},"wakeup_time":{"type":"string","format":"date-time","nullable":true},"time_zone":{"type":"string","nullable":true,"description":"The zone the band night was staged in (null for demo rows)"}}}}}},"400":{"description":"Malformed date"},"404":{"description":"No sleep data found for the specified date"}}}},"/api/health/stress-chart":{"get":{"summary":"Get stress chart data with HRV (Heart Rate Variability)","description":"Returns HRV data for stress visualization","tags":["Health"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"date","schema":{"type":"string","format":"date"},"description":"Date for stress data (defaults to today)"},{"in":"query","name":"granularity","schema":{"type":"string","enum":["day","week","month"],"default":"day"},"description":"- day: Average HRV per hour of the day\n- week: Average HRV per day for the last 7 days\n- month: Average HRV per day for the last 30 days\n"},{"in":"query","name":"time_zone","schema":{"type":"string"},"description":"IANA zone the hours and day boundaries are bucketed in (defaults to UTC)"}],"responses":{"200":{"description":"Stress chart data retrieved successfully"}}}},"/api/health/workouts":{"post":{"summary":"Save a workout recorded by the app's live workout screen","description":"Idempotent by the client-generated `id`: the app keeps an ended\nworkout on the phone and retries until this accepts it, so a replay\nof a save that already landed answers 200 with `created: false`.\nHeart-rate samples are stored with the row and drive the workout's\ndetail chart; avg and max heart rate are computed from them here.\n","tags":["Health"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id","workout_type","start_time","end_time"],"properties":{"id":{"type":"string","format":"uuid"},"workout_type":{"type":"string","enum":["running","walking","cycling","swimming","strength","yoga","hiit","other"]},"sport":{"type":"string","maxLength":40,"description":"Name shown in the app, e.g. Tennis"},"start_time":{"type":"string","format":"date-time"},"end_time":{"type":"string","format":"date-time"},"active_seconds":{"type":"integer","description":"Elapsed time excluding pauses"},"heart_rate_samples":{"type":"array","maxItems":20000,"items":{"type":"object","properties":{"t":{"type":"string","format":"date-time"},"bpm":{"type":"integer","minimum":25,"maximum":250}}}},"pauses":{"type":"array","maxItems":1000,"description":"When the workout was paused; the detail leaves the band's readings from these out","items":{"type":"object","required":["start","end"],"properties":{"start":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"}}}}}}}}},"responses":{"200":{"description":"Already saved under this id or for this session"},"201":{"description":"Saved"},"400":{"description":"The body failed validation"},"409":{"description":"The id belongs to another user's workout"}}}},"/api/health/workout-history":{"get":{"summary":"Get workout history list","description":"Returns the user's most recent workouts grouped by calendar date, newest date first, with the single most recent workout repeated as `last_routine`. `grouped_workouts` is always an array; it is empty (and `last_routine` is null, `total_workouts` is 0) when the user has no workouts.","tags":["Health"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"limit","schema":{"type":"integer","default":50},"description":"Maximum number of workouts to return (most recent first)"}],"responses":{"200":{"description":"Workout history retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"last_routine":{"type":"object","nullable":true,"description":"Most recent workout, or null when there are none","properties":{"id":{"type":"string"},"workout_type":{"type":"string","description":"Display name, e.g. Running"},"workout_type_raw":{"type":"string","description":"Stored type, e.g. running"},"calories_burned":{"type":"integer"},"duration_minutes":{"type":"integer"},"duration_formatted":{"type":"string"},"active_seconds":{"type":"integer","nullable":true,"description":"Exact active time in seconds (pauses excluded) for an app-recorded workout, else null"},"start_time":{"type":"string","format":"date-time"},"time_ago":{"type":"string"}}},"grouped_workouts":{"type":"array","description":"One entry per calendar date, newest first","items":{"type":"object","properties":{"date":{"type":"string","example":"2026-09-01"},"date_label":{"type":"string","example":"September 1, 2026"},"workouts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"workout_type":{"type":"string"},"workout_type_raw":{"type":"string"},"calories_burned":{"type":"integer"},"duration_minutes":{"type":"integer"},"duration_formatted":{"type":"string"},"distance_meters":{"type":"number","description":"0 when the workout has no distance"},"distance_formatted":{"type":"string","nullable":true},"start_time":{"type":"string","format":"date-time"},"time_ago":{"type":"string"}}}}}}},"total_workouts":{"type":"integer","description":"Number of workouts returned, 0 when none"}}}}}}}}},"/api/health/workout-detail/{id}":{"get":{"summary":"Get detailed workout information with heart rate chart","description":"Returns detailed workout data including heart rate timeline during the workout","tags":["Health"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Workout ID"}],"responses":{"200":{"description":"Workout detail retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"workout":{"type":"object","properties":{"id":{"type":"string"},"workout_type":{"type":"string"},"start_time":{"type":"string"},"end_time":{"type":"string"},"duration_minutes":{"type":"integer"},"duration_formatted":{"type":"string"},"calories_burned":{"type":"number"},"calories_estimated":{"type":"boolean","description":"True when calories_burned is estimated rather than measured, from heart rate (Keytel et al. 2005), or for a workout without heart rate, from its sport (2024 Adult Compendium of Physical Activities MET x weight x active hours)"},"steps":{"type":"integer","nullable":true,"description":"Band steps between start and end; null until the band has synced past the end"},"distance_meters":{"type":"number"},"distance_estimated":{"type":"boolean","description":"True when distance_meters is estimated from band steps and height (running, walking and hiking recorded in the app) rather than recorded"}}},"heart_rate_stats":{"type":"object","properties":{"min_bpm":{"type":"integer"},"max_bpm":{"type":"integer"},"avg_bpm":{"type":"integer"}}},"heart_rate_chart":{"type":"array","items":{"type":"object","properties":{"timestamp":{"type":"string"},"time_label":{"type":"string"},"heart_rate":{"type":"integer"},"source":{"type":"string","enum":["live","band"],"description":"App-recorded workouts only. live = read by the phone during the workout; band = the band's own 60-second window (none from paused time)"},"duration_seconds":{"type":"integer","description":"App-recorded workouts only. The time this reading stands for"}}}},"band_data":{"type":"object","nullable":true,"description":"App-recorded workouts only (null otherwise)","properties":{"status":{"type":"string","enum":["synced","waiting","unavailable"],"description":"synced: the band's data for the workout has all arrived.\nwaiting: not yet, and the workout ended under a day ago.\nunavailable: not yet, and no longer expected.\n"}}},"activity_difficulty":{"type":"number","description":"Activity difficulty score (0-10)"}}}}}},"404":{"description":"Workout not found"}}}},"/api/health/nutrition/seven-days-progress":{"get":{"summary":"Get seven-day nutrition progress scores for progress bars","description":"Returns exactly 7 days (3 before + given date + 3 after) with calories and steps progress scores.","tags":["Health"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"date","required":true,"schema":{"type":"string","format":"date"},"description":"Center date in YYYY-MM-DD format"}],"responses":{"200":{"description":"Seven-day progress bar data","content":{"application/json":{"schema":{"type":"object","properties":{"given_date":{"type":"string","format":"date"},"calories_progress_score":{"type":"number","description":"Average calories progress score (0-100)"},"steps_progress_score":{"type":"number","description":"Average steps progress score (0-100)"},"progress":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","format":"date"},"calories_progress_score":{"type":"number","example":70},"steps_progress_score":{"type":"number","example":80}}}}}}}}},"400":{"description":"Invalid or missing date"}}}},"/api/meals/foods/search":{"get":{"summary":"Search foods (local food_items + configured provider, e.g. FatSecret)","description":"Returns unified lightweight search hits. Local (cached/custom) foods come first with a uuid `id`; provider hits have `id: null` and must be imported via POST /api/meals/foods/import before logging. Provider outages never fail the request; local results are returned with `provider_error: true`.\n","tags":["Meals"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"q","schema":{"type":"string"},"description":"Search term"},{"in":"query","name":"page","schema":{"type":"number","default":1},"description":"1-based page number (provider results)"},{"in":"query","name":"pageSize","schema":{"type":"number","default":10},"description":"Max results per source"},{"in":"query","name":"category","schema":{"type":"string","enum":["breakfast","lunch","dinner","snack","beverage"]},"description":"Filter by category (local results)"},{"in":"query","name":"food_type","schema":{"type":"string"},"description":"Filter by food type (local results)"}],"responses":{"200":{"description":"Unified food search hits","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"foods":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","nullable":true},"external_source":{"type":"string","nullable":true},"external_id":{"type":"string","nullable":true},"name":{"type":"string"},"brand":{"type":"string","nullable":true},"calories":{"type":"number","nullable":true},"basis_description":{"type":"string","nullable":true}}}},"count":{"type":"number"},"page":{"type":"number"},"pageSize":{"type":"number"},"provider":{"type":"string","nullable":true},"provider_error":{"type":"boolean"}}}}}}}}},"/api/meals/foods/import":{"post":{"summary":"Import a provider food into the local database (import-on-select)","description":"Fetches full detail from the external provider, upserts it into food_items (canonical store) and returns the local row with a uuid id. Nutrient columns are totals for serving_size_g in serving_size_unit.\n","tags":["Meals"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["external_source","external_id"],"properties":{"external_source":{"type":"string","enum":["fatsecret"]},"external_id":{"type":"string"}}}}}},"responses":{"201":{"description":"Food imported"},"400":{"description":"Invalid body"},"404":{"description":"Provider does not know this id"},"503":{"description":"No food data provider configured"}}}},"/api/meals/foods/barcode/{code}":{"get":{"summary":"Look up a food by barcode (local cache first, then provider)","tags":["Meals"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"code","required":true,"schema":{"type":"string"},"description":"GTIN/UPC/EAN barcode (zero-padded to GTIN-13 for the provider)"}],"responses":{"200":{"description":"Food found"},"404":{"description":"Not found. `barcode_lookup_supported` is false when the provider does not offer barcode lookup (FatSecret premier tier) or no provider is configured.\n"}}}},"/api/meals/foods/recognize":{"post":{"summary":"Estimate the foods in a photo","description":"Returns search-hit shaped candidates for the foods visible in an image, so the client can reuse the normal pick-and-import flow. Currently DORMANT: the provider account has no image-recognition scope, so this answers 503 image_recognition_unavailable.\n","tags":["Meals"],"security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Candidate foods, most prominent first (NOT ranked by confidence)"},"400":{"description":"No image, or the image was unreadable"},"503":{"description":"Photo estimates are not enabled for this deployment"}}}},"/api/meals/foods/{id}":{"get":{"summary":"Get food item details","tags":["Meals"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Food item ID"}],"responses":{"200":{"description":"Food item details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodItem"}}}}}}},"/api/meals/foods":{"post":{"summary":"Create custom food item with image","description":"Creates a private custom food owned by the caller (created_by_user_id = current user, visibility = 'private'), so it only surfaces in the owner's searches. This endpoint is the seam for the future custom-foods/recipes and sharing feature (visibility 'private' | 'shared' | 'public').\n","tags":["Meals"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["name","category","calories"],"properties":{"name":{"type":"string"},"brand":{"type":"string"},"category":{"type":"string","enum":["breakfast","lunch","dinner","snack","beverage"]},"food_type":{"type":"string"},"serving_size_g":{"type":"number"},"serving_size_unit":{"type":"string"},"calories":{"type":"number"},"protein_g":{"type":"number"},"carbs_g":{"type":"number"},"fat_g":{"type":"number"},"saturated_fat_g":{"type":"number","nullable":true},"fiber_g":{"type":"number"},"sugar_g":{"type":"number"},"ingredients":{"type":"array","items":{"type":"string"},"description":"List of ingredients in this food item","example":["Chicken breast","Olive oil","Salt","Pepper"]},"prep_time_minutes":{"type":"number","description":"Food preparation time in minutes","example":15},"image":{"type":"string","format":"binary","description":"Food image file (max 5MB)"}}}}}},"responses":{"201":{"description":"Food item created"}}}},"/api/meals":{"post":{"summary":"Log a meal","tags":["Meals"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["meal_type","food"],"properties":{"meal_type":{"type":"string","enum":["breakfast","lunch","dinner","snack"]},"meal_name":{"type":"string"},"meal_date":{"type":"string","format":"date"},"meal_time":{"type":"string","format":"time"},"food_item_id":{"type":"string","format":"uuid","description":"Canonical food_items row to log. When sent with `quantity`, the backend recomputes all nutrition totals server-side and ignores client-sent totals.\n"},"quantity":{"type":"number","description":"Amount consumed, in the food's serving_size_unit"},"quantity_unit":{"type":"string","description":"Unit for quantity (defaults to the food's serving_size_unit)"},"image":{"type":"string","format":"binary","description":"Food image file (max 5MB) - will be attached to the `food` object as `image_url`"},"food":{"type":"object","description":"Food item details (from create food or search)","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"brand":{"type":"string"},"category":{"type":"string"},"food_type":{"type":"string"},"serving_size_g":{"type":"number"},"serving_size_unit":{"type":"string"},"calories":{"type":"number"},"protein_g":{"type":"number"},"carbs_g":{"type":"number"},"fat_g":{"type":"number"},"saturated_fat_g":{"type":"number","nullable":true},"fiber_g":{"type":"number"},"sugar_g":{"type":"number"},"sodium_mg":{"type":"number"},"cholesterol_mg":{"type":"number"},"vitamin_a_mcg":{"type":"number"},"vitamin_c_mg":{"type":"number"},"vitamin_d_mcg":{"type":"number"},"calcium_mg":{"type":"number"},"iron_mg":{"type":"number"},"potassium_mg":{"type":"number"},"barcode":{"type":"string"},"image_url":{"type":"string"},"verified":{"type":"boolean"},"source":{"type":"string"},"is_active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"ingredients":{"type":"array","items":{"type":"string"}},"prep_time_minutes":{"type":"number"}}}}}}}},"responses":{"201":{"description":"Meal logged successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Meal"}}}}}},"get":{"summary":"Get user's meals","tags":["Meals"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"start_date","schema":{"type":"string","format":"date"},"description":"Start date filter"},{"in":"query","name":"end_date","schema":{"type":"string","format":"date"},"description":"End date filter"},{"in":"query","name":"meal_type","schema":{"type":"string","enum":["breakfast","lunch","dinner","snack"]},"description":"Filter by meal type"}],"responses":{"200":{"description":"Meals retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"meals":{"type":"array","items":{"$ref":"#/components/schemas/Meal"}}}}}}}}}},"/api/meals/preferences":{"get":{"summary":"Get user dietary preferences","tags":["Meals"],"security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Dietary preferences"}}},"put":{"summary":"Update dietary preferences","tags":["Meals"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"diet_type":{"type":"string"},"allergies":{"type":"array","items":{"type":"string"}},"food_dislikes":{"type":"array","items":{"type":"string"}},"daily_calorie_target":{"type":"number"},"daily_protein_target_g":{"type":"number"},"daily_carbs_target_g":{"type":"number"},"daily_fat_target_g":{"type":"number"},"daily_water_target_ml":{"type":"number"}}}}}},"responses":{"200":{"description":"Preferences updated"}}}},"/api/meals/{id}/upload-image":{"post":{"summary":"Upload image for a meal","tags":["Meals"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Meal ID"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"image":{"type":"string","format":"binary","description":"Meal image file (max 5MB)"}}}}}},"responses":{"200":{"description":"Image uploaded successfully"}}}},"/api/meals/{id}":{"put":{"summary":"Update a meal (e.g., add/change image, notes)","tags":["Meals"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Meal ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"meal_name":{"type":"string"},"notes":{"type":"string"},"image_url":{"type":"string"},"location":{"type":"string"},"difficulty":{"type":"string","enum":["easy","medium","hard"]},"prep_time_minutes":{"type":"number"},"cook_time_minutes":{"type":"number"},"total_time_minutes":{"type":"number"},"rating":{"type":"number","minimum":1,"maximum":5},"ingredients":{"type":"array","items":{"type":"string"}}}},"example":{"image_url":"https://example.com/meal-photo.jpg","notes":"Delicious home-cooked meal","difficulty":"medium","prep_time_minutes":20,"cook_time_minutes":30,"total_time_minutes":50,"rating":4,"ingredients":["Chicken","Rice","Vegetables"]}}}},"responses":{"200":{"description":"Meal updated successfully"}}},"delete":{"summary":"Delete a meal","tags":["Meals"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Meal ID"}],"responses":{"200":{"description":"Meal deleted successfully"}}}},"/api/meals/nutrition/daily":{"get":{"summary":"Get daily nutrition summary","tags":["Meals"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"date","schema":{"type":"string","format":"date"},"description":"Date (defaults to today)"}],"responses":{"200":{"description":"Daily nutrition summary with macronutrient breakdown","content":{"application/json":{"schema":{"type":"object","properties":{"summary_date":{"type":"string","format":"date"},"total_calories":{"type":"number"},"total_protein_g":{"type":"number"},"total_carbs_g":{"type":"number"},"total_fat_g":{"type":"number"},"total_water_ml":{"type":"number"},"breakfast_count":{"type":"number"},"lunch_count":{"type":"number"},"dinner_count":{"type":"number"},"snack_count":{"type":"number"},"macros":{"type":"object","properties":{"protein":{"type":"object","properties":{"consumed":{"type":"number"},"target":{"type":"number"},"remaining":{"type":"number"},"percentage":{"type":"number"}}},"carbs":{"type":"object","properties":{"consumed":{"type":"number"},"target":{"type":"number"},"remaining":{"type":"number"},"percentage":{"type":"number"}}},"fats":{"type":"object","properties":{"consumed":{"type":"number"},"target":{"type":"number"},"remaining":{"type":"number"},"percentage":{"type":"number"}}},"calories":{"type":"object","properties":{"consumed":{"type":"number"},"target":{"type":"number"},"remaining":{"type":"number"},"percentage":{"type":"number"}}},"fiber":{"type":"object","properties":{"consumed":{"type":"number"},"target":{"type":"number"},"remaining":{"type":"number"},"percentage":{"type":"number"}}},"water":{"type":"object","properties":{"consumed":{"type":"number"},"target":{"type":"number"},"remaining":{"type":"number"},"percentage":{"type":"number"}}}}}}}}}}}}},"/api/meals/water":{"post":{"summary":"Log water intake","tags":["Meals"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount_ml"],"properties":{"amount_ml":{"type":"number","example":250},"date":{"type":"string","format":"date"}}}}}},"responses":{"201":{"description":"Water intake logged"}}}},"/api/meals/suggestions":{"get":{"summary":"Get AI meal suggestions","tags":["Meals"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"type","schema":{"type":"string","enum":["single_meal","daily_plan","recipe","alternative"],"default":"single_meal"},"description":"Type of suggestion"},{"in":"query","name":"meal_type","schema":{"type":"string","enum":["breakfast","lunch","dinner","snack"]},"description":"Specific meal type (optional, auto-detected from time if not provided)"}],"responses":{"200":{"description":"AI meal suggestions","content":{"application/json":{"schema":{"type":"object","properties":{"suggestions":{"type":"array","items":{"type":"object"}}}}}}}}}},"/api/notifications":{"get":{"summary":"Get user notifications","description":"The inbox: notifications that were sent (status sent, delivered or read) and are meant for it. Queued reminders and outbox-only pushes (sync nudges) are not listed. `unread_count` uses the same filter. Does not require completed onboarding.\n","tags":["Notifications"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["unread","read","all"],"default":"all"},"description":"Filter notifications by read status"},{"in":"query","name":"type","schema":{"type":"string","enum":["health_alert","goal_reminder","achievement","social","system","marketing"]},"description":"Filter by notification type"},{"in":"query","name":"page","schema":{"type":"integer","minimum":1,"default":1}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Notifications retrieved successfully"}}}},"/api/notifications/send":{"post":{"summary":"Send a notification to yourself","description":"Queues the notification in the push outbox and, unless schedule_at is in the future, delivers it in the request to every registered installation of the user (falling back to the legacy fcm_token). Future-scheduled notifications are delivered by the dispatcher at their time. schedule_at may be at most 7 days ahead, and a user may have at most 10 notifications from this endpoint waiting to be sent.\n","tags":["Notifications"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","message","type"],"properties":{"title":{"type":"string","maxLength":100},"message":{"type":"string","maxLength":500},"type":{"type":"string","enum":["health_alert","goal_reminder","achievement","social","system"]},"priority":{"type":"string","enum":["low","medium","high","urgent"],"default":"medium"},"schedule_at":{"type":"string","format":"date-time","description":"Optional future delivery time, at most 7 days ahead"},"metadata":{"type":"object"}}}}}},"responses":{"201":{"description":"Notification row created. Delivery outcome is in `push`; `notification.status` is the row's status afterwards (pending when scheduled or deferred, sent, failed, skipped, suppressed, ...).\n","content":{"application/json":{"schema":{"type":"object","properties":{"notification":{"$ref":"#/components/schemas/Notification"},"push":{"type":"object","description":"Always the same shape.","properties":{"attempted":{"type":"boolean","description":"true when FCM was called"},"sent":{"type":"boolean"},"reason":{"type":"string","nullable":true,"description":"null when sent; 'scheduled'; a delivery reason such as 'no_tokens', 'permission_denied', 'user_disabled' or 'paused'; or an FCM messaging/* code"},"message_id":{"type":"string","nullable":true},"token_cleared":{"type":"boolean","description":"true when FCM reported a stored token dead and it was removed"},"status_recorded":{"type":"boolean","description":"false only if the push was sent but the row could not be updated"}}}}}}}},"429":{"description":"too_many_waiting: 10 notifications from this endpoint are already waiting to be sent"}}}},"/api/notifications/{notificationId}/read":{"post":{"summary":"Mark notification as read","tags":["Notifications"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"notificationId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Notification marked as read"}}}},"/api/notifications/read-all":{"post":{"summary":"Mark all notifications as read","tags":["Notifications"],"security":[{"BearerAuth":[]}],"responses":{"200":{"description":"All notifications marked as read"}}}},"/api/notifications/preferences":{"get":{"summary":"Get notification preferences","tags":["Notifications"],"security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Notification preferences retrieved successfully"}}},"put":{"summary":"Update notification preferences","tags":["Notifications"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Partial map keyed by notification type (health_alerts, goal_reminders, activity_summary, social_updates, system_updates, plan_reminders, sync_nudges). Omitted fields keep their stored value.","additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean"},"frequency":{"type":"string","enum":["immediate","hourly","daily","weekly"]},"quiet_hours":{"type":"object","nullable":true,"description":"Wall-clock window in the user's time zone; null clears it","properties":{"start_time":{"type":"string","example":"22:00"},"end_time":{"type":"string","example":"07:00"}}}}},"example":{"plan_reminders":{"enabled":true,"quiet_hours":{"start_time":"22:00","end_time":"07:00"}},"sync_nudges":{"enabled":false}}}}}},"responses":{"200":{"description":"Notification preferences updated successfully"}}}},"/api/notifications/{notificationId}":{"delete":{"summary":"Delete notification","tags":["Notifications"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"notificationId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Notification deleted successfully"}}}},"/api/notifications/test":{"post":{"summary":"Send test notification (development only)","tags":["Notifications"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","default":"Test Notification"},"message":{"type":"string","default":"This is a test notification"},"dry_run":{"type":"boolean","default":false,"description":"Ask FCM to validate the token and payload without delivering"}}}}}},"description":"Sends straight through FCM (not the outbox) to the installation the user used most recently, or to the legacy app_user.fcm_token when no installation is registered. `target` in the response says which.\n","responses":{"200":{"description":"Test notification sent (or validated, when dry_run)"},"502":{"description":"FCM rejected the send; `code` carries the messaging/* reason and `hint` explains it"}}}},"/api/onboarding-questions":{"get":{"summary":"Get all onboarding questions with answer options","tags":["Onboarding"],"responses":{"200":{"description":"Onboarding questions retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"questions":{"type":"array","items":{"$ref":"#/components/schemas/OnboardingQuestion"}}}}}}}}}},"/api/push/installation":{"put":{"summary":"Register or refresh this app installation for push","description":"Upserts the installation (keyed by installation_id, which the app generates once). Reporting a token another installation holds moves it here. app_state=foreground also records that the user has the app open (used to hold back sync nudges).\n","tags":["Push"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["installation_id","platform"],"properties":{"installation_id":{"type":"string","minLength":8,"maxLength":128,"pattern":"^[A-Za-z0-9._:-]+$"},"fcm_token":{"type":"string","nullable":true,"maxLength":4096,"description":"null = no token yet; a token already stored for this installation is kept"},"platform":{"type":"string","enum":["ios","android"]},"firebase_project":{"type":"string","nullable":true,"maxLength":100},"app_version":{"type":"string","nullable":true,"maxLength":50},"locale":{"type":"string","nullable":true,"maxLength":35},"timezone":{"type":"string","nullable":true,"description":"IANA zone, stored under its canonical name; an unknown zone is stored as null"},"authorization_status":{"type":"string","enum":["authorized","provisional","denied","not_determined","ephemeral","unknown"]},"capabilities":{"type":"array","maxItems":20,"items":{"type":"string","maxLength":40}},"app_state":{"type":"string","enum":["foreground","background"],"default":"background"}}}}}},"responses":{"200":{"description":"The stored installation, the user's notification preferences and the server clock"}}}},"/api/push/installation/{installation_id}":{"delete":{"summary":"Unregister this installation (sign-out)","description":"Also stops the band sync nudges when this installation reported the band's latest drain, until a drain is reported again.\n","tags":["Push"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"installation_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ removed: boolean } — false when this user had no such installation"}}}},"/api/push/scheduled":{"get":{"summary":"Upcoming reminders the iOS app may schedule as local notifications","description":"Pending reminders due between 2 minutes and 48 hours from now. Empty when plan reminders are off for the user or pushes are paused (pushNotifications off); the app then cancels what it mirrored earlier on its next sync. Reminders that would fall in the user's quiet hours are left to the server, which defers them, and reminders of a plan the app no longer shows are left out.\n","tags":["Push"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"installation_id","schema":{"type":"string"}}],"responses":{"200":{"description":"{ server_time, reminders: [{ id, type, scheduled_at, expires_at, title, body, deep_link, collapse_key }] }"}}}},"/api/push/scheduled/mirror":{"put":{"summary":"Replace this installation's acks of locally scheduled reminders","description":"The listed reminders are scheduled on this device; the server will not also push them to it. Acks of this installation for pending reminders not in the list are removed, except for reminders due within 2 minutes or already due: their local copy may have fired already.\n","tags":["Push"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["installation_id","notification_ids"],"properties":{"installation_id":{"type":"string"},"notification_ids":{"type":"array","maxItems":64,"items":{"type":"string","format":"uuid"}}}}}}},"responses":{"200":{"description":"{ mirrored: uuid[] } — the listed ids now acked for this installation, leaving out reminders the server withdrew (cancelled, expired, suppressed, skipped)"},"404":{"description":"The installation does not belong to this user"}}}},"/api/push/events":{"post":{"summary":"Report that a push was received or opened on this installation","tags":["Push"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["installation_id","notification_id","event"],"properties":{"installation_id":{"type":"string"},"notification_id":{"type":"string","format":"uuid"},"event":{"type":"string","enum":["received","opened"]},"occurred_at":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"{ ok: true }"},"404":{"description":"No such notification for this user"}}}},"/api/push/sync-report":{"post":{"summary":"Report a successful band drain (or that the band was unpaired)","description":"Feeds the band sync nudges. Does not count as the app being in the foreground.","tags":["Push"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["installation_id"],"properties":{"installation_id":{"type":"string"},"tracker_key":{"type":"string","nullable":true,"maxLength":128},"drained_at":{"type":"string","format":"date-time","description":"required unless paired is false"},"records_received":{"type":"integer","nullable":true,"minimum":0},"battery_percent":{"type":"integer","nullable":true,"minimum":0,"maximum":100},"charging":{"type":"boolean","nullable":true},"used_sectors":{"type":"integer","nullable":true},"total_sectors":{"type":"integer","nullable":true},"trigger":{"type":"string","nullable":true,"maxLength":64},"paired":{"type":"boolean","default":true}}}}}},"responses":{"200":{"description":"{ episode_closed, unpaired }. episode_closed: this report ended a stale episode. unpaired: this report (paired false) marked the band unpaired. It is false when the unpair came from an installation that does not hold the band (the band's latest drain came from another installation that is still registered) and names no band, or a different one: it is then ignored, and the app should report it again later.\n"}}}},"/api/stripe/create-checkout-session":{"post":{"summary":"Create a Stripe Checkout session for a subscription plan","tags":["Stripe"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["plan_id"],"properties":{"plan_id":{"type":"string","format":"uuid","description":"Subscription plan ID to purchase"}}}}}},"responses":{"200":{"description":"Checkout session created","content":{"application/json":{"schema":{"type":"object","properties":{"sessionId":{"type":"string"},"url":{"type":"string","description":"Redirect user to this URL for payment"}}}}}},"400":{"description":"Validation error or plan not found"},"401":{"description":"Unauthorized"}}}},"/api/stripe/create-portal-session":{"post":{"summary":"Create a Stripe Billing Portal session for subscription management","tags":["Stripe"],"security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Portal session created","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"Redirect user to this URL for subscription management"}}}}}},"401":{"description":"Unauthorized"}}}},"/api/stripe/cancel":{"post":{"summary":"Cancel the user's active Stripe subscription","tags":["Stripe"],"security":[{"BearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"immediately":{"type":"boolean","default":false,"description":"If true cancel now; otherwise cancel at period end"}}}}}},"responses":{"200":{"description":"Subscription cancellation initiated"},"400":{"description":"No active Stripe subscription"},"401":{"description":"Unauthorized"}}}},"/api/subscriptions/plans":{"get":{"summary":"Get all available subscription plans","tags":["Subscriptions"],"responses":{"200":{"description":"List of subscription plans","content":{"application/json":{"schema":{"type":"object","properties":{"plans":{"type":"array","items":{"$ref":"#/components/schemas/SubscriptionPlan"}}}}}}}}}},"/api/subscriptions/plans/{identifier}":{"get":{"summary":"Get a specific subscription plan","tags":["Subscriptions"],"parameters":[{"in":"path","name":"identifier","required":true,"schema":{"type":"string"},"description":"Plan ID or plan name"}],"responses":{"200":{"description":"Subscription plan details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionPlan"}}}},"404":{"description":"Plan not found"}}}},"/api/subscriptions/my-subscription":{"get":{"summary":"Get current user's active subscription","tags":["Subscriptions"],"security":[{"BearerAuth":[]}],"responses":{"200":{"description":"User's active subscription","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSubscription"}}}},"401":{"description":"Unauthorized"}}}},"/api/subscriptions/current":{"get":{"summary":"Get current subscription with full feature information","description":"Returns current subscription tier, status, and all available features for frontend UI/UX decisions","tags":["Subscriptions"],"security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Current subscription with features","content":{"application/json":{"schema":{"type":"object","properties":{"subscription":{"$ref":"#/components/schemas/UserSubscription"},"features":{"type":"array","items":{"type":"string"}},"tier":{"type":"string","enum":["free","premium"]},"isActive":{"type":"boolean"},"daysRemaining":{"type":"number"},"canAccess":{"type":"object","description":"Feature access matrix. Subscription gating was removed, so every feature reports true for every user; retained so existing clients keep parsing the response.\n","example":{"nutrition_tracking":true,"ai_insights":true,"advanced_analytics":true}}}}}}}}}},"/api/subscriptions/my-features":{"get":{"summary":"Get features available to current user","tags":["Subscriptions"],"security":[{"BearerAuth":[]}],"responses":{"200":{"description":"List of features user has access to","content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/subscriptions/features":{"get":{"summary":"Get all available features","tags":["Subscriptions"],"responses":{"200":{"description":"List of all features","content":{"application/json":{"schema":{"type":"object","properties":{"features":{"type":"array","items":{"type":"object"}}}}}}}}}},"/api/subscriptions/check-feature/{featureCode}":{"get":{"summary":"Check if user has access to a specific feature","tags":["Subscriptions"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"featureCode","required":true,"schema":{"type":"string"},"description":"Feature code to check"}],"responses":{"200":{"description":"Feature access status","content":{"application/json":{"schema":{"type":"object","properties":{"has_access":{"type":"boolean"},"feature_code":{"type":"string"}}}}}}}}},"/api/subscriptions/trial/check-eligibility/{planId}":{"get":{"summary":"Check if user is eligible for free trial","tags":["Subscriptions"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"path","name":"planId","required":true,"schema":{"type":"string","format":"uuid"},"description":"Plan ID to check trial eligibility for"}],"responses":{"200":{"description":"Trial eligibility status","content":{"application/json":{"schema":{"type":"object","properties":{"eligible":{"type":"boolean"},"plan_id":{"type":"string"}}}}}}}}},"/api/subscriptions/trial/start":{"post":{"summary":"Start a free trial","tags":["Subscriptions"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["plan_id"],"properties":{"plan_id":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Trial started successfully","content":{"application/json":{"schema":{"type":"object","properties":{"subscription_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"trial_start":{"type":"string","format":"date-time"},"trial_end":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Invalid request or user not eligible for trial"}}}},"/api/subscriptions/subscribe":{"post":{"summary":"Subscribe to a plan","tags":["Subscriptions"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["plan_id"],"properties":{"plan_id":{"type":"string","format":"uuid"},"payment_provider":{"type":"string","enum":["stripe","apple","google","manual"],"default":"manual"},"payment_provider_id":{"type":"string"}}}}}},"responses":{"200":{"description":"Subscription created successfully"},"400":{"description":"Invalid request"}}}},"/api/subscriptions/cancel":{"post":{"summary":"Cancel current subscription","tags":["Subscriptions"],"security":[{"BearerAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"immediately":{"type":"boolean","default":false,"description":"If true, cancel immediately. Otherwise, cancel at period end."}}}}}},"responses":{"200":{"description":"Subscription canceled"},"400":{"description":"No active subscription found"}}}},"/api/subscriptions/history":{"get":{"summary":"Get subscription history","tags":["Subscriptions"],"security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Subscription history","content":{"application/json":{"schema":{"type":"object","properties":{"subscriptions":{"type":"array","items":{"$ref":"#/components/schemas/UserSubscription"}}}}}}}}}},"/api/subscriptions/payments":{"get":{"summary":"Get payment history","tags":["Subscriptions"],"security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Payment history","content":{"application/json":{"schema":{"type":"object","properties":{"payments":{"type":"array","items":{"type":"object"}}}}}}}}}},"/api/subscriptions/feature-usage":{"get":{"summary":"Get feature usage statistics","tags":["Subscriptions"],"security":[{"BearerAuth":[]}],"parameters":[{"in":"query","name":"start_date","schema":{"type":"string","format":"date"}},{"in":"query","name":"end_date","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Feature usage statistics"}}}},"/api/v1/sync":{"post":{"summary":"Sync telemetry and fitness data","description":"Receives and processes data from smartwatch devices or fitness trackers.\nSupports three formats:\n1. **FitnessTrackerData (NEW)**: Complete daily fitness summary with time-series, workouts, and sleep - idempotent by user+date\n2. **Device batch format**: Send multiple telemetry readings in the `items` array - efficient for offline sync\n3. **Device direct format**: Send single reading as direct object properties - simpler for real-time data\n","tags":["Sync"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/FitnessTrackerData"},{"$ref":"#/components/schemas/DeviceSyncRequest"}]},"examples":{"fitnessTrackerData":{"summary":"Fitness Tracker Data (Idempotent Daily Sync)","value":{"device_id":"WATCH-ABC-12345","date":"2026-01-29","time_series_metrics":[{"timestamp":"2026-01-29T08:00:00Z","heart_rate":72,"hrv":45,"respiratory_rate":16.5,"spo2":98,"skin_temp_c":34.1},{"timestamp":"2026-01-29T09:00:00Z","heart_rate":85,"hrv":42,"spo2":97}],"daily_cumulatives":{"steps":10524,"total_calories_burned":2340,"active_calories_burned":680,"distance_meters":8420.5,"floors_climbed":12},"detected_workouts":[{"workout_type":"run","start_time":"2026-01-29T07:00:00Z","end_time":"2026-01-29T07:45:00Z","average_heart_rate":145,"calories_burned":420,"intensity_score":8.5},{"workout_type":"walk","start_time":"2026-01-29T12:00:00Z","end_time":"2026-01-29T12:30:00Z","average_heart_rate":95,"calories_burned":120,"intensity_score":4.2}],"sleep_data":{"total_sleep_duration_minutes":456,"sleep_score":85,"stages":{"rem_minutes":98,"deep_minutes":112,"light_minutes":230,"awake_minutes":16},"bedtime":"2026-01-28T22:30:00Z","wakeup_time":"2026-01-29T06:06:00Z"}}},"deviceBatchFormat":{"summary":"Device Batch Format (telemetry)","value":{"deviceId":"DEV-WEARABLE-002","batchClientId":"batch-20251105-001","items":[{"timestamp":"2025-11-05T08:00:00Z","ppg":{"heartRate":82,"spO2":97},"bloodPressure":{"systolic_mmHg":118,"diastolic_mmHg":76},"deviceStatus":{"batteryLevel":88,"bleConnection":"connected"}}]}},"deviceDirectFormat":{"summary":"Device Direct Format (single reading)","value":{"deviceId":"DEV-WEARABLE-001","timestamp":"2025-10-16T08:26:15Z","ppg":{"heartRate":80,"spO2":98},"deviceStatus":{"batteryLevel":91,"bleConnection":"connected"}}}}}}},"responses":{"200":{"description":"Sync successful","content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"status":{"type":"string","example":"ok"},"type":{"type":"string","example":"fitness"},"user_id":{"type":"string","example":"user123"},"date":{"type":"string","example":"2026-01-29"},"batchId":{"type":"string","format":"uuid"}}},{"type":"object","properties":{"status":{"type":"string","example":"ok"},"type":{"type":"string","example":"device"},"batchId":{"type":"string","format":"uuid"}}}]}}}},"400":{"description":"Bad request - validation error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"validationErrors":{"type":"array","items":{"type":"string"}}}},"examples":{"fitnessValidationError":{"summary":"Fitness payload validation error","value":{"error":"Invalid fitness payload","validationErrors":["user_id is required and must be a string","date is required and must be a string (YYYY-MM-DD)"]}},"deviceMissingId":{"summary":"Device missing deviceId","value":{"error":"deviceId is required"}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"string"}}}}}}}}},"/api/users/register":{"post":{"summary":"Register a new user with Firebase authentication","tags":["Users"],"security":[{"FirebaseAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["firebase_uid","email","first_name","last_name"],"properties":{"firebase_uid":{"type":"string"},"email":{"type":"string","format":"email"},"first_name":{"type":"string","minLength":1,"maxLength":50},"last_name":{"type":"string","minLength":1,"maxLength":50},"display_name":{"type":"string","deprecated":true,"description":"Legacy field, ignored — display_name is derived from first_name/last_name"},"phone_number":{"type":"string"},"fcm_token":{"type":"string"},"profile_image_url":{"type":"string"}}}}}},"responses":{"201":{"description":"User registered successfully","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/User"},"token":{"type":"string"}}}}}},"400":{"description":"Validation error"},"409":{"description":"User already exists"}}}},"/api/users/login":{"post":{"summary":"Login user with Firebase token","tags":["Users"],"security":[{"FirebaseAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"fcm_token":{"type":"string"}}}}}},"responses":{"200":{"description":"Login successful","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/User"},"token":{"type":"string"}}}}}},"401":{"description":"Invalid Firebase token"},"404":{"description":"User not found"}}}},"/api/users/verification-email":{"post":{"summary":"Send the branded email-verification message","description":"Generates a Firebase verification link, rewrites it onto this API's domain (so it can open in the hiyd app) and emails it. The recipient is always taken from the caller's Firebase ID token, never from the request body. Callable before backend registration, since during signup the user exists in Firebase only.\n","tags":["Users"],"security":[{"FirebaseAuth":[]}],"responses":{"200":{"description":"Email sent, or the address was already verified","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"sent":{"type":"boolean"},"alreadyVerified":{"type":"boolean"}}}}}},"401":{"description":"Missing or invalid Firebase token"},"429":{"description":"Too many verification emails requested"}}}},"/api/users/password-reset-email":{"post":{"summary":"Send the branded password-reset message","description":"Public (the caller is signed out). Answers identically whether or not an account exists for the address, so it cannot be used to discover which emails have hiyd accounts. The link lands on the hosted /auth/reset-password page, where the new password is chosen.\n","tags":["Users"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"Sent if the account exists (the response does not say)"},"400":{"description":"Missing or malformed email address"},"429":{"description":"Too many requests from this client or for this address"},"503":{"description":"No mail provider configured; clients fall back to Firebase"}}}},"/api/users/profile":{"get":{"summary":"Get current user profile","tags":["Users"],"security":[{"BearerAuth":[]}],"responses":{"200":{"description":"User profile retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}}}},"put":{"summary":"Update user profile","tags":["Users"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"first_name":{"type":"string","minLength":1,"maxLength":50},"last_name":{"type":"string","minLength":1,"maxLength":50},"display_name":{"type":"string","deprecated":true,"description":"Legacy field — when first_name/last_name are sent, display_name is re-derived server-side"},"phone_number":{"type":"string"},"date_of_birth":{"type":"string","format":"date"},"gender":{"type":"string","enum":["male","female","non_binary","other","prefer_not_to_say"]},"height_cm":{"type":"number"},"weight_kg":{"type":"number"},"target_weight_kg":{"type":"number"},"activity_level":{"type":"string","enum":["sedentary","light","moderate","active","very_active"]},"fitness_goals":{"type":"array","items":{"type":"string"}},"daily_calorie_target":{"type":"number"},"timezone":{"type":"string"},"preferred_language":{"type":"string"},"privacy_settings":{"type":"object"}}}}}},"responses":{"200":{"description":"Profile updated successfully"}}}},"/api/users/profile/fitness-goals-options":{"get":{"summary":"Get available fitness goal options for profile selection","tags":["Users"],"responses":{"200":{"description":"Fitness goal options retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"fitness_goals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}}}}}}}}}}},"/api/users/profile/activity-levels":{"get":{"summary":"Get available activity level options for profile selection","tags":["Users"],"responses":{"200":{"description":"Activity level options retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"activity_levels":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}}}}}}}}}}}},"/api/users/profile/image":{"post":{"summary":"Upload profile image","tags":["Users"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"image":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"Profile image uploaded successfully"}}}},"/api/users/onboarding":{"post":{"summary":"Complete user onboarding","tags":["Onboarding"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["date_of_birth","gender","height_cm","weight_kg","responses"],"properties":{"date_of_birth":{"type":"string","format":"date","example":"1990-01-01"},"gender":{"type":"string","enum":["male","female","non_binary","other","prefer_not_to_say"],"example":"male"},"height_cm":{"type":"number","example":175},"weight_kg":{"type":"number","example":70},"target_weight_kg":{"type":"number","description":"Optional target weight","example":68},"responses":{"type":"array","description":"Array of question responses. Each entry carries either answer_ids (choice/grid/skin questions) or response_text (text questions), never both.","items":{"type":"object","required":["question_id"],"properties":{"question_id":{"type":"string","format":"uuid","example":"00000000-0000-0000-0000-000000000101"},"answer_ids":{"type":"array","description":"Array of selected answer IDs (single item for single-choice, multiple for multi-choice)","items":{"type":"string","format":"uuid"},"example":["answer-uuid-1","answer-uuid-2"]},"response_text":{"type":"string","maxLength":280,"description":"Free-text answer, only valid for text-type questions","example":"I want to sleep better and have more energy."}}}}}},"example":{"date_of_birth":"1990-01-01","gender":"male","height_cm":175,"weight_kg":70,"target_weight_kg":68,"responses":[{"question_id":"00000000-0000-0000-0000-000000000101","answer_ids":["answer-uuid-1"]},{"question_id":"00000000-0000-0000-0000-000000000107","response_text":"I want to sleep better and have more energy."}]}}}},"responses":{"200":{"description":"Onboarding completed successfully"}}}},"/api/users/onboarding/responses":{"put":{"summary":"Replace onboarding question responses after onboarding is complete","description":"POST /users/onboarding is one-shot. Use this to change answers afterwards. Only the questions present in the body are touched; their previous answers are replaced.","tags":["Users"],"security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["responses"],"properties":{"responses":{"type":"array","description":"Same item shape as /users/onboarding. Each entry carries either answer_ids or response_text, never both.","items":{"type":"object","required":["question_id"],"properties":{"question_id":{"type":"string","format":"uuid"},"answer_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"response_text":{"type":"string","maxLength":280}}}}}},"example":{"responses":[{"question_id":"00000000-0000-0000-0000-000000000101","answer_ids":["answer-uuid-2"]},{"question_id":"00000000-0000-0000-0000-000000000107","response_text":"Mostly better sleep."}]}}}},"responses":{"200":{"description":"Responses replaced"},"400":{"description":"Validation failed, or onboarding not completed yet (use POST /users/onboarding)"}}}},"/api/users/logout":{"post":{"summary":"Logout user","tags":["Users"],"security":[{"BearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"fcm_token":{"type":"string","description":"The FCM token this device holds. When given, only that token is cleared."}}}}}},"responses":{"200":{"description":"Logout successful"}}}},"/api/users/delete":{"delete":{"summary":"Delete user account","tags":["Users"],"security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Account deleted successfully"}}}},"/api/users/delete-with-firebase":{"delete":{"summary":"Delete current user account and Firebase user (Authorized)","tags":["Users"],"security":[{"BearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","description":"Email to delete (must match authenticated user's email)"}}}}}},"responses":{"200":{"description":"User and Firebase account deleted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"deleted_user":{"type":"object"}}}}}},"401":{"description":"Unauthorized - Token required"},"403":{"description":"Forbidden - Email does not match authenticated user"},"404":{"description":"User not found"},"500":{"description":"Server error"}}}},"/.well-known/apple-app-site-association":{"get":{"summary":"iOS Universal Links association file","description":"Lets iOS open /auth/action links directly in the hiyd app when it is installed. Served on the API domain the verification emails link to.\n","tags":["Deep Linking"],"responses":{"200":{"description":"Association JSON for Apple's CDN"}}}},"/.well-known/assetlinks.json":{"get":{"summary":"Android App Links association file","description":"Lets Android open /auth/action links directly in the hiyd app when it is installed. Requires ANDROID_SHA256_FINGERPRINTS to be configured with the signing-certificate fingerprints.\n","tags":["Deep Linking"],"responses":{"200":{"description":"Digital Asset Links JSON"},"404":{"description":"Signing fingerprints not configured"}}}},"/":{"get":{"summary":"Health check endpoint","description":"Returns the current status and timestamp of the API","tags":["Health"],"responses":{"200":{"description":"API is healthy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthCheck"}}}}}}}}}