Bloodwork Tracker API Documentation
External API reference reviewed against version 55 on 2026-09-08. Examples use synthetic data; abbreviated responses omit additional schema fields.
Base URL: https://health-tracker.project4.net/api
Authentication
Send one of these headers with external API requests:
X-API-Key: bwt_... Authorization: Bearer bwt_...
Administrators create keys under API. Full keys are shown once only. Keys permit read, create, update and delete operations: they are not read-only and have no per-key read-only scope. Keep them out of public client code.
Patient Scope
Every clinical endpoint is restricted to the issuing account's personal patient and an active owner membership. Administrator status does not bypass this boundary. Client-supplied patient IDs do not select another patient. Sharing and patient switching are not enabled.
Bootstrap includes patient: {"id":1,"displayName":"Personal record","selfUserId":1}. IDs may be numeric strings. Revoked patient access returns 403.
Response Codes
Success Codes
Routed JSON data responses include X-BWT-Code. Objects also include a top-level code; arrays keep their array body. OPTIONS returns empty 204, and the separate public /api/build.php metadata endpoint is outside this contract.
BWT_SUCCESS BWT_CREATED BWT_OK
Error Body
{
"code": "BWT_API_KEY_INVALID",
"message": "Invalid or missing API key"
}
BWT_INVALID_JSON Request body could not be parsed as JSON BWT_VALIDATION_FAILED Required fields or values are missing or invalid BWT_INVALID_DATE Date is not valid YYYY-MM-DD BWT_INVALID_NUMBER Number field is not numeric BWT_INVALID_OTP One-time login code is invalid BWT_API_KEY_INVALID API key is missing, inactive, revoked, or incorrect BWT_UNAUTHORIZED Browser session is missing or invalid BWT_FORBIDDEN Authenticated user cannot perform the action BWT_NOT_FOUND Route or generic resource not found BWT_TEST_NOT_FOUND Blood test not found for this user/key BWT_RESULT_NOT_FOUND Result not found for this user/key BWT_CONFLICT Request conflicts with current state BWT_RATE_LIMITED Too many requests BWT_SERVER_ERROR Unexpected server error
Schemas
SI is the default display. Results retain the original value and add measurement with original laboratory units/reference bounds, SI equivalents, conversion provenance and normalization status. Read measurement.original.unit when interpreting a result. Legacy snapshots are labelled legacy_assay_inferred. Unavailable conversions remain null.
Count units display as ×10⁹/L and ×10¹²/L. Assay writes accept ASCII and superscript variants, canonicalized to x10^9/L or x10^12/L with existing unit IDs and unchanged numeric values. Original reported labels and JSON exports are not prettified. Browser bootstrap supplies unitCatalog entries (id, unit) for input suggestions; no new external endpoint is added.
Result writes accept optional reported: {"unit":"mg/dL","low":70,"high":100}. Enter the value in that reported unit. Omission on an edit preserves stored metadata; omission on creation uses assay defaults. Result displays.native/si/conventional contain available display measurements. Preferences: si (default), conventional, both (SI + US), native.
measurement.status is normalized or needs_review; needs_review has null SI/conventional displays. Original origin is reported, assay_defaults or legacy_assay_inferred. Conversion versions identify frozen conversion rules, not app releases. Numeric database fields may be JSON numbers or numeric strings.
BloodTest
{
"id": 123,
"testName": "Haemoglobin",
"groupName": "GENERAL HAEMATOLOGY",
"unitOfMeasure": "g/L",
"unitId": "g_l",
"analyteId": "haemoglobin",
"normalizedUnit": "g/L",
"analyteName": "Haemoglobin",
"refRangeLow": 135,
"refRangeHigh": 180,
"targetZoneEnabled": true,
"targetZoneLow": 145,
"targetZoneHigh": 165,
"targetZoneUnit": "g/L",
"archivedAt": null,
"archivedReason": "",
"conversions": [
{
"unitId": "g_dl",
"unit": "g/dL",
"multiplyBy": 0.1,
"isApproximate": false,
"source": "Derived metric prefix conversion",
"notes": ""
}
],
"createdAt": "2026-05-06 10:00:00+00"
}
Normalized unit fields are included when a marker matches the built-in analyte catalog. unitOfMeasure remains the original/native unit text for backward compatibility. Target zones are visualization-only optimal ranges and do not affect low, high, or in-range status. Archived assays include archivedAt and archivedReason.
Result (Expanded Example)
{
"id": 456,
"testId": 123,
"testDate": "2026-05-06",
"value": 151,
"measurement": {
"original": {"value":151,"unit":"g/L","unitId":"g_l","analyteId":"haemoglobin","low":135,"high":180,"origin":"reported"},
"si": {"value":151,"unit":"g/L","unitId":"g_l","low":135,"high":180},
"conversion": {"factor":1,"version":"identity-v1","source":"Same unit"},
"status":"normalized"
},
"displays": {
"native": {"value":151,"unit":"g/L","unitId":"g_l","analyteId":"haemoglobin","low":135,"high":180,"origin":"reported"},
"si": {"value":151,"unit":"g/L","unitId":"g_l","low":135,"high":180},
"conventional": {"value":15.1,"unit":"g/dL","unitId":"g_dl","low":13.5,"high":18}
},
"createdAt": "2026-05-06 10:00:00+00"
}
External Endpoints
Write And Archive Semantics
- PUT and PATCH use the same handler, not partial-update semantics. Assays require testName and unitOfMeasure; send every definition field you wish to retain. Omitted group, bounds and target settings reset to defaults.
- Result updates require testId, testDate and value; moving a result to a different assay is rejected. Omit reported to retain its original metadata.
- When changing reported.unit, also send low/high in that unit (or nulls). Omitted bounds are inherited, not converted. reported.analyteId may identify a catalog analyte; calculated SI values are read-only.
- Result POST upserts on assay/date, returning 201/BWT_CREATED even for an existing date. Omitted reported retains metadata on that upsert.
- Lists and bootstrap default to active. Use status=all or archived for archived history. Individual ID reads/writes/deletes still address archived records: archive is not revocation.
- No external archive/unarchive route exists; archivedAt in a write body does not archive an assay. Use the signed-in app.
- Lists have no pagination. Result from/to filters are inclusive date-string comparisons; use zero-padded YYYY-MM-DD and validate dates in your client.
- DELETE is permanent; deleting an assay also deletes its results. All samples are examples, not instructions to modify a production patient.
| Operation | Success | Other common failures |
|---|---|---|
| GET bootstrap/tests/results lists | 200 BWT_SUCCESS (header for arrays) | 400 invalid status |
| GET tests/{id}, results/{id} | 200 BWT_SUCCESS | 404 |
| POST tests | 201 BWT_CREATED | 400 |
| PUT/PATCH tests/{id} | 200 BWT_SUCCESS | 400, 404 |
| POST results | 201 BWT_CREATED, including duplicate-date replacement | 400, 404 |
| PUT/PATCH results/{id} | 200 BWT_SUCCESS | 400, 404 |
| DELETE tests/{id}, results/{id} | 200 BWT_OK | 404 |
| GET/PUT clinical-notes | 200 BWT_SUCCESS | 400 malformed JSON on PUT |
These are common outcomes, not exhaustive validation guarantees. Handle all non-2xx responses. Supported 409/422/429 error mappings do not mean every endpoint enforces those checks. Shell examples below use Bash syntax; Windows users should adapt quoting or use Git Bash.
Returns patient context, tests, results and clinical notes. status defaults to active; use all for archived history too.
curl "https://health-tracker.project4.net/api/external/bootstrap?status=all" \ -H "Authorization: Bearer $BWT_API_KEY"
{
"code": "BWT_SUCCESS",
"patient": {"id":1,"displayName":"Personal record","selfUserId":1},
"tests": [],
"results": [],
"clinicalNotes": "Patient context..."
}List blood tests. Optional status query values are active (default), archived, and all.
curl "https://health-tracker.project4.net/api/external/tests?status=active" \ -H "Authorization: Bearer $BWT_API_KEY"
[
{
"id": 123,
"testName": "Haemoglobin",
"groupName": "GENERAL HAEMATOLOGY",
"unitOfMeasure": "g/L",
"archivedAt": null,
"archivedReason": "",
"refRangeLow": 135,
"refRangeHigh": 180,
"targetZoneEnabled": true,
"targetZoneLow": 145,
"targetZoneHigh": 165,
"targetZoneUnit": "g/L",
"createdAt": "2026-05-06 10:00:00+00"
}
]Create a blood test.
curl -X POST https://health-tracker.project4.net/api/external/tests \
-H "Authorization: Bearer $BWT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"testName": "Ferritin",
"groupName": "IRON STUDIES",
"unitOfMeasure": "ug/L",
"refRangeLow": 30,
"refRangeHigh": 400,
"targetZoneEnabled": true,
"targetZoneLow": 80,
"targetZoneHigh": 180,
"targetZoneUnit": "ug/L"
}'{
"testName": "Ferritin",
"groupName": "IRON STUDIES",
"unitOfMeasure": "ug/L",
"refRangeLow": 30,
"refRangeHigh": 400,
"targetZoneEnabled": true,
"targetZoneLow": 80,
"targetZoneHigh": 180,
"targetZoneUnit": "ug/L"
}{
"code": "BWT_CREATED",
"id": 124,
"testName": "Ferritin",
"groupName": "IRON STUDIES",
"unitOfMeasure": "ug/L",
"refRangeLow": 30,
"refRangeHigh": 400,
"targetZoneEnabled": true,
"targetZoneLow": 80,
"targetZoneHigh": 180,
"targetZoneUnit": "ug/L",
"createdAt": "2026-05-06 10:00:00+00"
}Return one test and its results.
curl https://health-tracker.project4.net/api/external/tests/123 \ -H "Authorization: Bearer $BWT_API_KEY"
{
"code": "BWT_SUCCESS",
"test": {
"id": 123,
"testName": "Haemoglobin",
"groupName": "GENERAL HAEMATOLOGY",
"unitOfMeasure": "g/L",
"refRangeLow": 135,
"refRangeHigh": 180,
"targetZoneEnabled": true,
"targetZoneLow": 145,
"targetZoneHigh": 165,
"targetZoneUnit": "g/L",
"createdAt": "2026-05-06 10:00:00+00"
},
"results": [
{
"id": 456,
"testId": 123,
"testDate": "2026-05-06",
"value": 151,
"createdAt": "2026-05-06 10:00:00+00"
}
]
}Replace a test definition.
curl -X PUT https://health-tracker.project4.net/api/external/tests/123 \
-H "Authorization: Bearer $BWT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"testName": "Ferritin",
"groupName": "IRON STUDIES",
"unitOfMeasure": "ug/L",
"refRangeLow": 30,
"refRangeHigh": 400,
"targetZoneEnabled": true,
"targetZoneLow": 80,
"targetZoneHigh": 180,
"targetZoneUnit": "ug/L"
}'{
"code": "BWT_SUCCESS",
"id": 123,
"testName": "Ferritin",
"groupName": "IRON STUDIES",
"unitOfMeasure": "ug/L",
"refRangeLow": 30,
"refRangeHigh": 400,
"targetZoneEnabled": true,
"targetZoneLow": 80,
"targetZoneHigh": 180,
"targetZoneUnit": "ug/L",
"createdAt": "2026-05-06 10:00:00+00"
}Update a test definition.
curl -X PATCH https://health-tracker.project4.net/api/external/tests/123 \
-H "Authorization: Bearer $BWT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"testName": "Ferritin",
"groupName": "IRON STUDIES",
"unitOfMeasure": "ug/L",
"refRangeLow": 30,
"refRangeHigh": 400,
"targetZoneEnabled": true,
"targetZoneLow": 80,
"targetZoneHigh": 180,
"targetZoneUnit": "ug/L"
}'{
"code": "BWT_SUCCESS",
"id": 123,
"testName": "Ferritin",
"groupName": "IRON STUDIES",
"unitOfMeasure": "ug/L",
"refRangeLow": 30,
"refRangeHigh": 400,
"targetZoneEnabled": true,
"targetZoneLow": 80,
"targetZoneHigh": 180,
"targetZoneUnit": "ug/L",
"createdAt": "2026-05-06 10:00:00+00"
}Delete a test and its results.
curl -X DELETE https://health-tracker.project4.net/api/external/tests/123 \ -H "Authorization: Bearer $BWT_API_KEY"
{ "code": "BWT_OK", "ok": true }List results. Optional query parameters: testId, from, to, and status. Archived assay results are hidden unless status=archived or status=all is requested.
curl "https://health-tracker.project4.net/api/external/results?testId=123&from=2026-01-01&to=2026-12-31&status=active" \ -H "Authorization: Bearer $BWT_API_KEY"
[
{
"id": 456,
"testId": 123,
"testDate": "2026-05-06",
"value": 151,
"createdAt": "2026-05-06 10:00:00+00"
}
]Create or upsert a dated result.
curl -X POST https://health-tracker.project4.net/api/external/results \
-H "Authorization: Bearer $BWT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"testId": 123,
"testDate": "2026-05-06",
"value": 151
}'{
"testId": 123,
"testDate": "2026-05-06",
"value": 151
}{
"code": "BWT_CREATED",
"id": 456,
"testId": 123,
"testDate": "2026-05-06",
"value": 151,
"createdAt": "2026-05-06 10:00:00+00"
}Return one result.
curl https://health-tracker.project4.net/api/external/results/456 \ -H "Authorization: Bearer $BWT_API_KEY"
{
"code": "BWT_SUCCESS",
"id": 456,
"testId": 123,
"testDate": "2026-05-06",
"value": 151,
"createdAt": "2026-05-06 10:00:00+00"
}Replace a result.
curl -X PUT https://health-tracker.project4.net/api/external/results/456 \
-H "Authorization: Bearer $BWT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"testId": 123,
"testDate": "2026-05-06",
"value": 152
}'{
"code": "BWT_SUCCESS",
"id": 456,
"testId": 123,
"testDate": "2026-05-06",
"value": 152,
"createdAt": "2026-05-06 10:00:00+00"
}Update a result.
curl -X PATCH https://health-tracker.project4.net/api/external/results/456 \
-H "Authorization: Bearer $BWT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"testId": 123,
"testDate": "2026-05-06",
"value": 152
}'{
"code": "BWT_SUCCESS",
"id": 456,
"testId": 123,
"testDate": "2026-05-06",
"value": 152,
"createdAt": "2026-05-06 10:00:00+00"
}Delete a result.
curl -X DELETE https://health-tracker.project4.net/api/external/results/456 \ -H "Authorization: Bearer $BWT_API_KEY"
{ "code": "BWT_OK", "ok": true }Return clinical notes.
curl https://health-tracker.project4.net/api/external/clinical-notes \ -H "Authorization: Bearer $BWT_API_KEY"
{
"code": "BWT_SUCCESS",
"clinicalNotes": "Patient context..."
}Update clinical notes.
curl -X PUT https://health-tracker.project4.net/api/external/clinical-notes \
-H "Authorization: Bearer $BWT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"clinicalNotes": "Updated patient context..."
}'{ "clinicalNotes": "Updated patient context..." }{
"code": "BWT_SUCCESS",
"clinicalNotes": "Updated patient context..."
}Browser Archive Routes
These internal browser routes require a valid session cookie, not an API key. Both return 200/BWT_SUCCESS with a full browser bootstrap under state; common failures are 401 (session), 403 (patient access), 404 (assay). Cookie jars are secrets. Request examples modify data.
curl -X POST https://health-tracker.project4.net/api/tests/123/archive \
-b cookies.txt -H "Content-Type: application/json" \
-d '{"archivedReason":"One-off assay"}'{"code":"BWT_SUCCESS","state":{"patient":{"id":1,"displayName":"Personal record","selfUserId":1},"tests":[],"results":[]}}curl -X POST https://health-tracker.project4.net/api/tests/123/unarchive \
-b cookies.txt -H "Content-Type: application/json" -d '{}'{"code":"BWT_SUCCESS","state":{"patient":{"id":1,"displayName":"Personal record","selfUserId":1},"tests":[],"results":[]}}Example
curl https://health-tracker.project4.net/api/external/tests \ -H "Authorization: Bearer $BWT_API_KEY"
Markdown reference remains available at API-DOCS.md.