Record a health observation
NRTR calls about 20 outside services from one internal boundary. Callers use NRTR authentication, NRTR JSON, and NRTR errors. This operation stores one observation at POST /internal/health/observations. That path is internal. It is not published on nrtr.health. Confirm the route in the repository before you ship a client. The field tables come first. The glucose task follows them.
Operation
POST /internal/health/observations
Creates one observation for the person named by the token. The service checks the body, stores the row in the local database, and returns the stored object. This call does not contact a lab vendor. A later job may sync. Failure of that job must not change the status of this call.
Authentication
Send a bearer token issued for the local environment. The token identifies the person. Do not send a vendor key on this route. Do not send a person identifier in the body. The service takes the person from the token so a client cannot write into someone else’s record by editing JSON.
Authorization: Bearer <token>
Content-Type: application/json
Accept: application/json
Missing header, malformed token, and expired token all return 401 with error.code set to unauthorized. The body does not say which of the three it was. The server log does. That split is deliberate: the caller can fix “get a new token,” and the log keeps the detail.
Request body
| Field | Type | Required | Rule |
|---|---|---|---|
code | string | Yes | A stable identifier such as loinc:2345-7. Free text is rejected. The service does not search names. |
value | number | Yes | A JSON number. The string "96" is rejected so the service does not guess a type. |
unit | string | Yes | The unit paired with that code. A glucose code with a unit that code does not allow fails validation. The service does not convert units in this call. |
observed_at | string | Yes | ISO-8601 with a numeric offset, for example 2026-09-24T14:10:00-06:00. Z is accepted. A time with no offset is rejected. |
source | string | No | One of manual, lab, device. Omitted means manual. |
profile_id | — | Forbidden | If present, the call fails validation. The person comes from the token. |
{
"code": "loinc:2345-7",
"value": 96,
"unit": "mg/dL",
"observed_at": "2026-09-24T14:10:00-06:00",
"source": "lab"
}
Success
201 Created. The body is the stored observation. It includes id and profile_id. value, unit, and observed_at come back unchanged. If you sent 96 and you get 96.0 as a number, that is the same value. If you sent milligrams per deciliter and you get millimoles per liter, the service has a bug. Do not “fix” it in the client.
{
"id": "obs_01JEXAMPLE",
"profile_id": "prf_local",
"code": "loinc:2345-7",
"value": 96,
"unit": "mg/dL",
"observed_at": "2026-09-24T14:10:00-06:00",
"source": "lab"
}
Errors
Every error body has error.code and error.message. Validation errors also have error.fields, a list of objects with name and reason. The shape does not change because a vendor failed. Vendor status codes stay in the server log.
| Status | error.code | When | What you do |
|---|---|---|---|
| 400 | validation_failed | A field breaks a rule in the table above. | Read error.fields. Fix those fields. Do not retry the same body. |
| 401 | unauthorized | The token is missing, malformed, or expired. | Get a new local token. Do not change the JSON. |
| 409 | duplicate_observation | The same code, time, value, and unit are already stored for this person. | Treat it as success if you were retrying a lost response. Otherwise change the time or confirm you meant to write twice. |
| 415 | unsupported_media_type | Content-Type is not application/json. | Set the header. A form post is not accepted. |
{
"error": {
"code": "validation_failed",
"message": "The observation was not stored.",
"fields": [
{ "name": "unit", "reason": "not_allowed_for_code" }
]
}
}
Read it back
GET /internal/health/observations/{id} with the same token. 200 returns the object. 404 with error.code of not_found means the identifier is wrong or it belongs to another person. The service does not use 404 to hide 401. An unauthenticated call is 401.
Task: store one value and read it back
Do this after the setup tutorial. You need NRTR_BASE (the service origin, no trailing slash) and NRTR_JWT (a local token).
- POST the glucose body.
- Require status 201. Copy
id. - GET that id with the same token.
- Compare
value,unit, andobserved_atto what you sent. They match, or the task fails. - POST the same body again. Require status 409 and
duplicate_observation.
Shell:
curl -sS -D - -o body.json -X POST "$NRTR_BASE/internal/health/observations" \
-H "Authorization: Bearer $NRTR_JWT" \
-H "Content-Type: application/json" \
-d '{"code":"loinc:2345-7","value":96,"unit":"mg/dL","observed_at":"2026-09-24T14:10:00-06:00","source":"lab"}'
Python:
import os
import requests
base = os.environ["NRTR_BASE"].rstrip("/")
token = os.environ["NRTR_JWT"]
body = {
"code": "loinc:2345-7",
"value": 96,
"unit": "mg/dL",
"observed_at": "2026-09-24T14:10:00-06:00",
"source": "lab",
}
created = requests.post(
f"{base}/internal/health/observations",
headers={"Authorization": f"Bearer {token}"},
json=body,
timeout=15,
)
created.raise_for_status()
obs_id = created.json()["id"]
fetched = requests.get(
f"{base}/internal/health/observations/{obs_id}",
headers={"Authorization": f"Bearer {token}"},
timeout=15,
)
fetched.raise_for_status()
got = fetched.json()
assert got["value"] == 96
assert got["unit"] == "mg/dL"
assert got["observed_at"] == body["observed_at"]
If curl returns 401, replace the token and send the same JSON. If it returns 400 and unit is in error.fields, the code and unit pair is wrong. Changing 96 to 95 will not fix it.
Why the contract is this strict
This section is explanation. It is not required to make the call.
Health numbers are easy to “clean up” in a client: trim a string, convert a unit, stamp “now” when the clock is missing. Each of those fixes destroys the original observation. The boundary refuses them so the row that lands in the database is the row the caller claims to have measured. Conversion, if it ever happens, is a different operation with its own inputs and its own stored result. It is not a side effect of a create.
The same rule is why vendor failures do not leak. A caller who branches on a lab company’s status code will break when we change companies. The caller branches on validation_failed, unauthorized, and duplicate_observation.