Documents / Observation contract

Requires a local service and a token

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

FieldTypeRequiredRule
codestringYesA stable identifier such as loinc:2345-7. Free text is rejected. The service does not search names.
valuenumberYesA JSON number. The string "96" is rejected so the service does not guess a type.
unitstringYesThe 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_atstringYesISO-8601 with a numeric offset, for example 2026-09-24T14:10:00-06:00. Z is accepted. A time with no offset is rejected.
sourcestringNoOne of manual, lab, device. Omitted means manual.
profile_id—ForbiddenIf 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.

Statuserror.codeWhenWhat you do
400validation_failedA field breaks a rule in the table above.Read error.fields. Fix those fields. Do not retry the same body.
401unauthorizedThe token is missing, malformed, or expired.Get a new local token. Do not change the JSON.
409duplicate_observationThe 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.
415unsupported_media_typeContent-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).

  1. POST the glucose body.
  2. Require status 201. Copy id.
  3. GET that id with the same token.
  4. Compare value, unit, and observed_at to what you sent. They match, or the task fails.
  5. 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.