API Reference

The Cloudflare Worker exposes a JSON REST API under the /api/ prefix. All responses have Content-Type: application/json and the following CORS headers:

Access-Control-Allow-Origin:  *
Access-Control-Allow-Methods: GET, POST, DELETE, PUT, OPTIONS
Access-Control-Allow-Headers: Content-Type

Blood Pressure

POST /api/blood-pressure

Record a new blood pressure measurement session. Exactly three readings are required. The Worker selects the best reading, calculates derived metrics, and assigns an AHA/ACC 2017 category.

Request Body

{
  "readings": [
    { "systolic": 122, "diastolic": 78, "pulse": 65 },
    { "systolic": 120, "diastolic": 76, "pulse": 63 },
    { "systolic": 121, "diastolic": 77, "pulse": 64 }
  ],
  "context": "morning",
  "notes": "After coffee",
  "tags": ["resting"]
}
FieldTypeRequiredConstraints
readingsArray<Reading>YesExactly 3 elements
readings[].systolicnumberYes50–300 mmHg; must be > diastolic
readings[].diastolicnumberYes30–200 mmHg
readings[].pulsenumberYes20–300 bpm
contextstringNoFree text
notesstringNoFree text
tagsstring[]NoMust be pre-existing tag names; defaults to []

Response — 201 Created

{
  "sessionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "bestReading": {
    "systolic": 120,
    "diastolic": 76,
    "pulse": 63,
    "pulsePressure": 44,
    "MAP": 90.7,
    "category": {
      "label": "Normal",
      "color": "#27ae60"
    }
  }
}

Errors

StatusMessage
400Exactly three readings are required
400Reading N: systolic must be between 50 and 300
400Reading N: diastolic must be less than systolic
400tags must be an array of strings

GET /api/blood-pressure

Retrieve a paginated list of blood pressure sessions, most recent first.

Query Parameters

ParameterDefaultMaxDescription
limit7100Number of sessions to return
offset0Number of sessions to skip

Response — 200 OK

{
  "sessions": [
    {
      "id": "3fa85f64-...",
      "recorded_at": "2026-03-22T07:14:00Z",
      "context": "morning",
      "notes": null,
      "readings": [
        {
          "reading_number": 1,
          "is_best": 0,
          "metrics": { "systolic": 122, "diastolic": 78, "pulse": 65 }
        },
        {
          "reading_number": 2,
          "is_best": 1,
          "metrics": {
            "systolic": 120,
            "diastolic": 76,
            "pulse": 63,
            "pulse_pressure": 44,
            "mean_arterial_pressure": 90.7
          }
        },
        {
          "reading_number": 3,
          "is_best": 0,
          "metrics": { "systolic": 121, "diastolic": 77, "pulse": 64 }
        }
      ],
      "category": { "label": "Normal", "color": "#27ae60" }
    }
  ],
  "limit": 7,
  "offset": 0
}

DELETE /api/blood-pressure/:id

Delete a measurement session and all associated observations and tags.

Path Parameters

ParameterDescription
:idUUID of the session to delete

Response — 200 OK

{ "deleted": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }

Errors

StatusMessage
400Session ID required

Weight

POST /api/weight

Record a new weight measurement.

Request Body

{
  "weight_kg": 82.5,
  "notes": "After morning run"
}
FieldTypeRequiredConstraints
weight_kgnumberYes20–300 kg
notesstringNoFree text

Response — 201 Created

{
  "id": "b5e0a1f2-...",
  "weight_kg": 82.5
}

Errors

StatusMessage
400weight_kg must be between 20 and 300

GET /api/weight

Retrieve weight entries, most recent first.

Query Parameters

ParameterDefaultMax
limit30100

Response — 200 OK

{
  "entries": [
    {
      "id": "b5e0a1f2-...",
      "weight_kg": 82.5,
      "notes": "After morning run",
      "recorded_at": "2026-03-22T07:30:00Z"
    }
  ]
}

DELETE /api/weight/:id

Delete a weight entry.

Path Parameters

ParameterDescription
:idUUID of the weight entry to delete

Response — 200 OK

{ "deleted": "b5e0a1f2-..." }

Profile

GET /api/profile

Retrieve the singleton user profile. Always returns a record (either the stored profile or a default with height_cm: null).

Response — 200 OK

{
  "id": 1,
  "height_cm": 178.0,
  "updated_at": "2026-01-10T09:00:00Z"
}

PUT /api/profile

Update the user’s height.

Request Body

{ "height_cm": 178.0 }
FieldTypeRequiredConstraints
height_cmnumberYes50–280 cm

Response — 200 OK

{ "height_cm": 178.0 }

Errors

StatusMessage
400height_cm must be between 50 and 280

Error Response Shape

All errors follow this shape:

{ "error": "Human-readable error message" }

Request Flow Diagram

sequenceDiagram participant C as Client participant W as Worker participant D as D1 C->>W: POST /api/blood-pressure W->>W: Parse JSON body W->>W: validateReadings() alt Validation fails W-->>C: 400 { error: "..." } else Valid W->>D: SELECT metric_types IN (...) W->>D: SELECT tags WHERE name IN (...) W->>D: batch([INSERT session, INSERT observations x11, INSERT session_tags]) D-->>W: OK W-->>C: 201 { sessionId, bestReading } end

External CPAP API (Read-Only)

The frontend also communicates with an external Worker at https://myair-sync.eddie-547.workers.dev. These endpoints are not part of this Worker but are documented here for completeness.

MethodPathDescription
GET/api/auth/statusCheck MyAir authentication status
POST/api/auth/startInitiate MyAir login (triggers email MFA)
POST/api/auth/verifySubmit MFA code to complete auth
POST/api/sync-nowTrigger immediate CPAP data sync
GET/api/cpapRetrieve stored CPAP records
GET/api/cpap-correlationCPAP data correlated with next-morning BP