Skip to content

API Reference ​

This document provides comprehensive information about the SparkyFitness API, including OpenAPI specification, endpoints, and integration guidelines.

OpenAPI Specification ​

The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to RESTful APIs. It allows both humans and computers to discover and understand the capabilities of a service without access to source code, documentation, or network traffic inspection.

Purpose in SparkyFitness ​

For SparkyFitness, an OpenAPI specification would serve several key purposes:

  • API Documentation: Provides clear and interactive documentation for all backend API endpoints, including request parameters, response structures, and authentication methods.
  • Client Code Generation: Enables automatic generation of client SDKs (Software Development Kits) in various programming languages, simplifying integration for third-party applications or mobile clients.
  • API Testing: Facilitates automated testing of API endpoints, ensuring consistency and correctness.
  • Design and Collaboration: Serves as a single source of truth for API design, improving collaboration between frontend and backend developers.

Key Elements of SparkyFitness's API (as described by OpenAPI) ​

An OpenAPI specification for SparkyFitness would detail:

  • Endpoints: All available API routes (e.g., /api/food, /api/users, /api/exercises).
  • Operations: HTTP methods supported for each endpoint (GET, POST, PUT, DELETE).
  • Parameters: Input parameters for each operation, including their types, formats, and whether they are required.
  • Responses: The structure of successful and error responses, including status codes and data models.
  • Authentication: How clients authenticate with the API (e.g., JWT tokens).
  • Data Models (Schemas): Definitions of the data structures used in requests and responses (e.g., FoodItem, UserProfile, ExerciseEntry).

Accessing the API Documentation ​

While a live, interactive OpenAPI documentation (like Swagger UI) might be available in development environments, the core API is consumed by the SparkyFitness frontend and can be explored by examining the backend routes in SparkyFitnessServer/routes/ and the service definitions in SparkyFitnessServer/services/.


Detailed API Endpoints ​

Food Assistant State ​

These additive routes are authenticated and owner-only, using the signed-in actor rather than a selected family diary. The shared FoodAssistant.api.zod.ts and FoodAssistant.zod.ts contracts define their bodies and responses.

RouteMethodPurpose
/api/v2/food-assistant/preferencesGET / PUTRead lasting preferences or create/edit one with its expected version
/api/v2/food-assistant/preferences/:key?version=NDELETEForget a preference only if its version still matches
/api/v2/food-assistant/tasksGET / POSTList recent tasks or create a task with a stable UUID request ID
/api/v2/food-assistant/tasks/:idGET / PATCHRead a full checkpoint or save one with an operation ID and expected version
/api/v2/food-assistant/tasks/:id/operationsGETRead committed before/after operation records
/api/v2/food-assistant/tasks/:id/:actionPOSTCancel or resume remaining work; action is cancel or resume

Task changes require operation_id and expected_version. Repeating an identical operation returns its stored result. Reusing an ID for different work, stale versions, and attempts to change a completed or cancelled task return HTTP 409. Domain writes must share the repository's operation transaction; the operation record and checkpoint commit together. Draft endpoints cannot mark a task complete or mutate diary/library records. Older chat clients do not need to send these new fields.

Authentication ​

  • API Key: Used for specific endpoints like /api/health-data. The API Key should be sent in the Authorization header as Bearer <API_KEY> or in the X-API-Key header. The API Key must have the necessary permissions (e.g., health_data_write).
  • JWT Token: Used for most authenticated endpoints. The JWT token should be sent in the Authorization header as Bearer <JWT_TOKEN>.

Error Handling ​

General error responses follow a consistent structure:

  • 400 Bad Request: Indicates invalid input or missing required fields.
  • 401 Unauthorized: Occurs when authentication credentials are missing or invalid.
  • 403 Forbidden: Occurs when the authenticated user does not have the necessary permissions.
  • 404 Not Found: The requested resource could not be found.
  • 500 Internal Server Error: An unexpected error occurred on the server.

Example Error Response:

json
{
  "error": "Error message describing the issue."
}

Endpoints ​

1. /api/health-data ​

  • Path: /api/health-data
  • Method: POST
  • Description: Submits various health data points (e.g., weight, steps, active calories, custom measurements) to the system. This endpoint is crucial for integrating SparkyFitness with external health tracking services like iOS Shortcuts and Android mobile apps, allowing seamless data submission.
  • Authentication: API Key (sent via Authorization header as Bearer <API_KEY> or X-API-Key header). The API Key must have health_data_write permission.
  • Request Body: An array of health data objects, or a single health data object.
    • Structure:
      json
      [
        {
          "value": <number>,
          "type": "<string>",
          "date": "<YYYY-MM-DD>",
          "timestamp": "<ISO 8601 string, optional>"
        },
        ...
      ]
    • Fields:
      • value (required, number): The measurement value.
      • type (required, string): The type of health data. This field is case-sensitive. Supported types include:
        • weight: Records body weight.
        • step: Records the number of steps taken.
        • water: Records water intake in milliliters.
        • Active Calories: Records calories burned through physical activity.
        • Any other string: Will be treated as a custom measurement category. If a category with this name does not exist, it will be automatically created.
      • date (required, string): The date of the measurement in YYYY-MM-DD format (e.g., "2025-08-19").
      • timestamp (optional, string): The exact timestamp of the measurement in ISO 8601 format (e.g., "2025-08-19T08:30:00Z"). If provided, the entryHour will be extracted for more granular tracking.
    • Examples:
      • Weight:
        json
        [
          {
            "value": 70.5,
            "type": "weight",
            "date": "2025-08-19"
          }
        ]
      • Steps:
        json
        [
          {
            "value": 10000,
            "type": "step",
            "date": "2025-08-19"
          }
        ]
      • Water Intake:
        json
        [
          {
            "value": 500,
            "type": "water",
            "date": "2025-08-19"
          }
        ]
      • Active Calories:
        json
        [
          {
            "value": 500,
            "type": "Active Calories",
            "date": "2025-08-19"
          }
        ]
      • Custom Measurement (e.g., "Blood Pressure Systolic"):
        json
        [
          {
            "value": 120,
            "type": "Blood Pressure Systolic",
            "date": "2025-08-19",
            "timestamp": "2025-08-19T08:30:00Z"
          }
        ]
  • Workout records (type: "Workout" or "ExerciseSession"): these carry a session object rather than a single value, and are written to exercise_entries (plus the telemetry tables below).
    • Core fields: activityType (string, becomes the exercise name and determines its modality), startTime/endTime (ISO 8601), duration (seconds), caloriesBurned, distance (kilometres), source, source_id (required for deduplication), sets[], raw_data, and optional record_timezone (IANA zone the workout was recorded in). The start time is stored as wall-clock entry_time in that zone, falling back to the profile timezone, and the zone used is saved on the entry as exercise_entries.record_timezone so reports convert it back correctly after travel.
    • X-Workout-Model-Version header: 2 or higher means set durations are in seconds; when absent they are read as minutes. 3 additionally signals that the optional wearable-telemetry objects below may be present. All telemetry fields are optional, so a client that omits them behaves exactly as before.
    • telemetry (object, optional): whole-session summary values. Keys are the exercise_entries column names and unknown keys are ignored. Commonly: avg_heart_rate, max_heart_rate, avg_speed_mps, max_speed_mps, avg_cadence, max_cadence, avg_power_watts, max_power_watts, elevation_gain_meters, elevation_loss_meters, min_elevation_meters, max_elevation_meters, floors_climbed, stroke_count, moving_time_seconds, elapsed_time_seconds, active_calories, ground_contact_time_ms, vertical_oscillation_mm, stride_length_cm. Any value the client omits is derived from the series below where possible; values the client does send are never overwritten.
    • gps_points[] (optional): the GPS track, written to exercise_entry_gps_points. Each point is { "t": ISO 8601, "lat": number, "lon": number } plus optional alt (m), speed (m/s), hr (bpm), cad, power (W), dist (cumulative metres), hacc/vacc (m), course (degrees). Points missing t, lat or lon are dropped.
    • hr_samples[] (optional): the heart-rate series as { "t": ISO 8601, "bpm": number }. Written to health_metric_samples under metric heart_rate, linked to the workout, and merged into the day's existing samples rather than replacing them. Send this in addition to any hr on gps_points so indoor workouts without GPS still produce a chart.
    • laps[] (optional): lap windows as { "lap_index": number, "start_time": ISO 8601, "end_time": ISO 8601 }, written to exercise_entry_laps. Send only the windows; per-lap distance, heart rate, speed, cadence, power and elevation are computed server-side from the series.
    • Heart-rate zones are derived server-side from the heart-rate series, using the person's own heart rate zone settings (GET/PUT /api/v2/heart-rate-zones: max and resting heart rate, zone method, custom floors) and falling back to a max HR estimated from the profile date of birth. They are written to exercise_entry_hr_zones together with the method used. A workout that already carries Apple's heart-rate zone group sends it as hr_zones (health sync) or appleZones (watch telemetry). Those seconds are stored with method device and the series is not bucketed again. Any other source still has no field for its own zones.
    • Clients should downsample before uploading. The reverse proxy caps request bodies at 10 MB; roughly 2000 GPS points and 1200 heart-rate samples per workout keeps a session near 250 KB.
    • Example:
      json
      [
        {
          "type": "ExerciseSession",
          "source": "HealthKit",
          "source_id": "9C84D9E1-0000-0000-0000-0000000000A1",
          "date": "2025-08-19",
          "startTime": "2025-08-19T09:00:00Z",
          "endTime": "2025-08-19T09:30:00Z",
          "duration": 1800,
          "activityType": "Outdoor Walk",
          "caloriesBurned": 150,
          "distance": 2.4,
          "telemetry": {
            "avg_heart_rate": 112,
            "max_heart_rate": 138,
            "elevation_gain_meters": 24
          },
          "gps_points": [
            { "t": "2025-08-19T09:00:00Z", "lat": 37.7749, "lon": -122.4194, "alt": 10.2, "speed": 1.31, "hr": 105 }
          ],
          "hr_samples": [
            { "t": "2025-08-19T09:00:00Z", "bpm": 105 }
          ],
          "laps": [
            { "lap_index": 1, "start_time": "2025-08-19T09:00:00Z", "end_time": "2025-08-19T09:15:00Z" }
          ]
        }
      ]
  • Responses:
    • 200 OK: returned whenever the request body itself is well-formed, even if some individual records could not be processed. Per-record outcomes are reported in the body: processed lists successful records, errors lists rejected records with their reasons, and skipped lists records that were intentionally not written (e.g. Nutrition records without a source_id, which cannot be deduplicated). errors and skipped are always present, possibly empty. A 200 with a non-empty errors array means the remaining records were still saved.
      json
      {
        "message": "All health data successfully processed.",
        "processed": [
          {
            "type": "weight",
            "status": "success",
            "data": { "id": "uuid", "user_id": "uuid", "entry_date": "2025-08-19", "weight": 70.5 }
          }
        ],
        "errors": [],
        "skipped": []
      }
      Partial failure example (still 200 OK):
      json
      {
        "message": "Some health data entries could not be processed.",
        "processed": [
          {
            "type": "weight",
            "status": "success",
            "data": { "id": "uuid", "user_id": "uuid", "entry_date": "2025-08-19", "weight": 70.5 }
          }
        ],
        "errors": [
          {
            "error": "Missing required fields: value (for scalar types), type, or date/timestamp in one of the entries",
            "entry": { "value": null, "type": "step", "date": "2025-08-19" }
          }
        ],
        "skipped": []
      }

      Breaking change note (v0.18+): earlier server versions returned 400 Bad Request with the { "message", "processed", "errors" } body when any record in the batch failed. Automations that keyed on the 400 status to detect partial failures should instead inspect the errors array of the 200 response.

    • 400 Bad Request: returned only for malformed request bodies (invalid JSON, or an array containing entries that are not non-null objects).
      json
      {
        "error": "Invalid JSON array format."
      }
      or
      json
      {
        "error": "Invalid health data format. All entries must be non-null objects."
      }
    • 401 Unauthorized:
      json
      {
        "error": "Unauthorized: Missing API Key"
      }
      or
      json
      {
        "error": "Unauthorized: Invalid or inactive API Key"
      }
    • 403 Forbidden:
      json
      {
        "error": "Forbidden: API Key does not have health_data_write permission"
      }

1a. /api/generic-health/samples ​

  • Path: /api/generic-health/samples
  • Method: POST
  • Description: Stores passive readings (all-day heart rate, stress, SpO2, HRV, respiration, body battery, skin temperature, blood glucose) from a source that has no server-side integration, in the same table GET /api/generic-health/samples reads. Each sample lands on the user's calendar day of its timestamp. A write only touches rows of the posted source, so readings from Garmin, Polar or a watch on the same day stay as they are.
  • Authentication: Session or API key with write access; requires the checkin permission when acting for another user.
  • Request Body:
    • metric (required): one of heart_rate, hrv, respiration, spo2, stress, body_battery, skin_temperature, blood_glucose.
    • source (required, string, 1–64 characters): stored as the provider name. The names the server writes samples under (manual, garmin, polar, intervals, intervals.icu, SparkyFitness Watch, HealthKit, apple_health, Health Connect) are refused, compared without case, spaces, dots, dashes and underscores. A passive write replaces the stored samples of its source (inside window for merge, the whole day for replace), except samples linked to a workout or sleep session (ex, sl), such as the heart rate a workout sent to /health-data stored under the same source. Those are kept in both modes.
    • device_name (optional, string).
    • mode (optional): merge (default) replaces only this source's stored samples inside window, on every day the window touches, and keeps the rest of those days, so sending the same window again replaces instead of duplicating; replace overwrites every day the samples touch.
    • window (required for merge): { "start": ISO 8601, "end": ISO 8601 }, at most 31 days. Every sample must lie inside it.
    • samples (required, up to 50000; empty only in merge mode, which then clears this source's samples inside window, for a window with no readings left): { "t": ISO 8601 with offset, ... } with the field of the metric: bpm (heart_rate), percentage (spo2), level (stress, body_battery), brpm (respiration), rmssd_ms/sdnn_ms (hrv), celsius (skin_temperature), mmol_l (blood_glucose). For blood_glucose, both t and mmol_l are required; mmol_l is a number greater than 0 and at most 100, always in mmol/L. source_bundle_id is optional. Convert mg/dL to mmol/L before uploading; account display units do not affect storage. Links to workouts or sleep (ex, sl) are not accepted.
  • Example Request Body:
    json
    {
      "metric": "heart_rate",
      "source": "xiaomi",
      "window": { "start": "2026-10-28T00:00:00+01:00", "end": "2026-10-29T00:00:00+01:00" },
      "samples": [
        { "t": "2026-10-28T06:00:00+01:00", "bpm": 58 },
        { "t": "2026-10-28T06:01:00+01:00", "bpm": 61 }
      ]
    }
  • Responses:
    • 200 OK: { "metric": "heart_rate", "source": "xiaomi", "mode": "merge", "days_written": 1, "samples_written": 2 }
    • 400 Bad Request: the body failed validation; issues names the field.
    • 403 Forbidden: no checkin permission for the active user, or a read-only API key.

2. /api/measurements/check-in ​

  • Path: /api/measurements/check-in

  • Method: POST

  • Description: Upserts (inserts or updates) daily check-in measurements for the authenticated user. Only one check-in entry is allowed per day.

  • Authentication: JWT Token (sent via Authorization header as Bearer <JWT_TOKEN>).

  • Request Body:

    • Structure:
      json
      {
        "entry_date": "<YYYY-MM-DD>",
        "weight": <number, optional>,
        "height": <number, optional>,
        "body_fat_percentage": <number, optional>,
        "neck": <number, optional>,
        "waist": <number, optional>,
        "hips": <number, optional>,
        "steps": <number, optional>,
        "muscle_mass_kg": <number, optional>,
        "bone_mass_kg": <number, optional>,
        "body_water_percentage": <number, optional>,
        "bmr": <number, optional>
      }
    • Fields:
      • entry_date (required, string): The date of the check-in in YYYY-MM-DD format.
      • Other fields (optional, number): Measurement values. Masses are in kilograms, circumferences and height in centimetres, bmr in kcal (enforced range 300–10,000 kcal), and body_fat_percentage / body_water_percentage are percentages.
      • BMI is not accepted or stored — it is derived from weight and height where it is displayed.
      • Sending null for a field clears a previously recorded value for that day.
  • Responses:

    • 200 OK:
      json
      {
        "message": "Check-in measurements upserted successfully.",
        "data": { "id": "uuid", "user_id": "uuid", "entry_date": "2025-08-19", "weight": 70.5 }
      }
    • 400 Bad Request:
      json
      {
        "error": "Entry date is required."
      }
    • 401 Unauthorized: (If JWT is missing or invalid)
    • 403 Forbidden: (If user is not authorized)
  • Path: /api/measurements/check-in/:date

  • Method: GET

  • Description: Retrieves daily check-in measurements for a specific date for the authenticated user.

  • Authentication: JWT Token.

  • Path Parameters:

    • date (required, string): The date of the check-in in YYYY-MM-DD format.
  • Responses:

    • 200 OK:
      json
      {
        "id": "uuid",
        "user_id": "uuid",
        "entry_date": "2025-08-19",
        "weight": 70.5,
        "body_fat_percentage": 15.2,
        "muscle_mass_kg": 34.2,
        "bone_mass_kg": 3.1,
        "body_water_percentage": 55.4
      }
      or {} if no entry found for the date.
    • 400 Bad Request:
      json
      {
        "error": "Date is required."
      }
    • 401 Unauthorized:
    • 403 Forbidden:
  • Path: /api/measurements/check-in/:entry_date

  • Method: PUT

  • Description: Updates an existing daily check-in measurement entry for a specific date.

  • Authentication: JWT Token.

  • Path Parameters:

    • entry_date (required, string): The date of the check-in in YYYY-MM-DD format.
  • Request Body:

    • Structure: Same as POST request for /api/measurements/check-in, but entry_date is provided in the path.
    • Fields: Any of the optional measurement fields (e.g., weight, body_fat_percentage, muscle_mass_kg, bone_mass_kg, body_water_percentage, neck, etc.) to update.
  • Responses:

    • 200 OK:
      json
      {
        "id": "uuid",
        "user_id": "uuid",
        "entry_date": "2025-08-19",
        "weight": 71.0,
        "body_fat_percentage": 15.2,
        "muscle_mass_kg": 34.2,
        "bone_mass_kg": 3.1,
        "body_water_percentage": 55.4
      }
    • 400 Bad Request:
      json
      {
        "error": "Entry date is required."
      }
    • 401 Unauthorized: (If JWT is missing or invalid)
    • 403 Forbidden: (If user is not authorized)
    • 404 Not Found:
      json
      {
        "error": "Check-in measurement not found or not authorized to update."
      }
  • Path: /api/measurements/check-in/:id

  • Method: DELETE

  • Description: Deletes a daily check-in measurement entry.

  • Authentication: JWT Token.

  • Path Parameters:

    • id (required, string): The ID of the check-in entry to delete.
  • Responses:

    • 200 OK:
      json
      {
        "message": "Check-in measurement deleted successfully."
      }
    • 400 Bad Request:
      json
      {
        "error": "Check-in Measurement ID is required."
      }
    • 401 Unauthorized:
    • 403 Forbidden:
    • 404 Not Found:
      json
      {
        "error": "Check-in measurement not found or not authorized to delete."
      }

3. /api/measurements/check-in-photos ​

Progress photos attached to a calendar day. Every route below requires the checkin permission; they are deliberately excluded from the GET → checkin_read downgrade applied to the rest of the measurements routes, so a family member with read-only access to the numbers still gets a 403 on the photos.

  • Path: /api/measurements/check-in-photos

  • Method: GET

  • Description: Every photo the caller has, newest day first, each paired with the weight logged on the same calendar day. Backs the mobile gallery and the side-by-side comparison and time-lapse on both mobile and web, in one request. The weight is joined on (user_id, entry_date) rather than the stored check_in_measurement_id, so a photo taken before that day's weight was entered still reports it.

  • Authentication: JWT Token.

  • Responses:

    • 200 OK: ordered by entry_date DESC, photo_type ASC. weight is in kilograms and is null when the day has no check-in measurement or that measurement carries no weight. file_path is omitted on purpose — image bytes come from /file/{id}, so the on-disk layout stays a server detail.
      json
      [
        { "id": "uuid", "entry_date": "2026-06-14", "photo_type": "front", "weight": 82.5 },
        { "id": "uuid", "entry_date": "2026-06-10", "photo_type": "side", "weight": null }
      ]
    • 401 Unauthorized:
    • 403 Forbidden:
  • Path: /api/measurements/check-in-photos/dates

  • Method: GET

  • Description: The calendar days that have at least one photo, so a date picker can mark them without pulling the whole gallery.

  • Authentication: JWT Token.

  • Responses:

    • 200 OK:
      json
      ["2026-06-14", "2026-06-10"]
  • Path: /api/measurements/check-in-photos/:date

  • Method: GET

  • Description: The photos taken on one day, including file_path.

  • Authentication: JWT Token.

  • Path Parameters:

    • date (required, string): YYYY-MM-DD.
  • Path: /api/measurements/check-in-photos/file/:id

  • Method: GET

  • Description: The image bytes for one photo. The uploads/check-in subtree is blocked on the public static mounts, so this authenticated route is the only way to read a photo.

  • Authentication: JWT Token.

  • Path: /api/measurements/check-in-photos/:date/:type

  • Method: POST

  • Description: Multipart upload of the field photo for one angle, replacing that angle if it is already taken. The server sniffs magic bytes and rejects anything that is not jpeg/png/gif/webp, so HEIC must be re-encoded by the client first.

  • Authentication: JWT Token.

  • Path Parameters:

    • date (required, string): YYYY-MM-DD.
    • type (required, string): one of front, back, side.
  • Responses:

    • 200 OK: the stored photo row, including file_path.
    • 400 Bad Request: no file provided, or a file whose bytes are not an accepted image.
  • Path: /api/measurements/check-in-photos/photo/:id

  • Method: DELETE

  • Description: Removes one photo.

  • Authentication: JWT Token.

  • Responses:

    • 204 No Content:

Timestamped custom measurement timelines ​

GET /api/measurements/custom-entries/{date}?byTimestamp=true reads the half-open midnight-to-midnight interval in the account timezone, using entry_timestamp instead of the imported entry_date. The read remains protected by the existing checkin permission and row-level security, and retains source/category metadata. Omit byTimestamp (or use false) to keep the existing calendar-date diary behavior; other values return HTTP 400. The interval follows daylight-saving changes.

Example passive glucose sample upload (the existing source/window rules also apply):

json
{
  "metric": "blood_glucose",
  "source": "dexcom-api",
  "mode": "merge",
  "window": { "start": "2026-11-03T10:00:00Z", "end": "2026-11-03T10:05:00Z" },
  "samples": [
    {
      "t": "2026-11-03T10:00:00Z",
      "mmol_l": 5.5,
      "source_bundle_id": "com.dexcom.g7"
    }
  ]
}

Released under the GPL-3.0 License.