DATEYE

Importing Measurements

POST /api/v1/import

Authenticated. The central endpoint. One request uploads a batch of 1–50 measurements for one patient. The mode discriminator determines how the patient is identified.

The four modes

The top-level object is a discriminated union on mode. Each variant is .strict()unknown fields cause 400 (deliberate, to catch adapter bugs early).

a) follow_up — known patient (already linked)

{ "mode": "follow_up", "mkClientId": "<uuid>", "measurements": [  ] }

The client knows the internal client UUID (from the mapping sync). Measurements are attributed directly to the patient. Precondition: valid consent (treatment consent) for the patient — otherwise 403 CONSENT_MISSING / 403 CONSENT_WITHDRAWN.

b) first_visit — unknown patient (first upload)

{
  "mode": "first_visit",
  "devicePseudonym": "<32 hex>",
  "initialsHint": "AB",
  "dobMonthYearHint": "1985-03",
  "measurements": [  ]
}

Lands in the pending queue for manual linking in the portal.

  • devicePseudonym: exactly 32 hex chars (^[0-9a-f]{32}$, case-insensitive, lowercased). See devicePseudonym below.
  • initialsHint: 1–3 uppercase letters ^[A-ZÄÖÜ]+$ (only a visual aid in the portal).
  • dobMonthYearHint: ^\d{4}-(0[1-9]|1[0-2])$ (birth month/year, only a visual aid).

c) pseudonym_only — pseudonym without hints (fallback)

{ "mode": "pseudonym_only", "devicePseudonym": "<32 hex>", "measurements": [  ] }

Measurements are accepted only if the pseudonym is already linked server-side. Otherwise no buffering → response with warning resend_with_hints (the client should retry with first_visit + hints).

d) token — persistent re-export (preferred for repeat sends)

{ "mode": "token", "persistentToken": "tk_<32 hex>", "devicePseudonym": "<32 hex>?", "measurements": [  ] }

persistentToken (^tk_[0-9a-f]{32}$) stably identifies an already-linked patient. An optional devicePseudonym is used only for stale detection (if it differs from the token’s patient, the token wins; warning stale_pseudonym). A foreign token (another practice) → 403 TOKEN_FOREIGN.

Measurement object (identical in all modes)

Required per measurement: eye (OD|OS), measuredAt (ISO-8601), sourceAdapterId (1–64), sourceDevice (1–128). Optional dateOnly: true (date without time → midnight UTC, portal shows date only). All clinical fields are optional/nullable; each has a plausibility min/max (range violation → 400).

GroupFields (unit)
SourcesourceAdapterId, sourceSerial?, sourceDataId?, sourceDevice, sourceDeviceName?
Refractionsphere (−30…30 D), cylinder (−15…15 D), axis (0…180°), visualAcuitySC/visualAcuityCC (0…2), cycloplegia (bool), method (≤32), addition (0…5 D), vertex (5…25 mm), prism (0…40), prismBase (0…360°), sphericalEquivalent (−30…30 D)
BiometryaxialLength (10…45 mm), anteriorChamberDepth (1…7 mm), lensThickness (1…8 mm), vitreousChamberDepth (5…35 mm), axialSnr (≥0), axialMeasurementCount (int ≥0), axialStandardDeviation (≥0), cornealThickness (100…1500 µm), choroidalThickness (10…1000 µm), pupilDiameter (1…12 mm), pupilLighting (≤32), pupilCenterX/pupilCenterY, cornealDiameter (5…18 mm), bodyHeight (30…250 cm)
KeratometrycornealAstigmatism (0…20 D), cornealAxis1/cornealAxis2 (0…180°), cornealRadius1/cornealRadius2 (4…12 mm)
Patientgender, ethnicity (fixed enums)

sourceSerial + sourceDataId are the building blocks of duplicate detection.

Success response (200)

The import always replies with a counter object (even on partial failures):

{
  "success": true,
  "data": {
    "total": 4,
    "uploaded": 3,
    "duplicate": 1,
    "assigned": 0,
    "unmatched": 3,
    "errors": 0,
    "results": [ { "eye": "OD", "status": "stored" }, { "eye": "OS", "status": "duplicate" } ],
    "persistent_token": null,
    "skip_pseudonyms": [ "a1b2…", "f5e6…" ],
    "warnings": [ { "code": "rejected_pseudonym", "message": "…" } ]
  }
}
FieldMeaning
totalNumber of measurements sent
uploadedNewly stored
duplicateDetected as duplicate (not stored again)
assignedDirectly attributed to a patient (only follow_up/token)
unmatchedPlaced in the pending queue (only first_visit)
errorsMeasurements without usable values, etc.
results[]Per measurement: eye + status (stored|duplicate|rejected) + optional errorCode
persistent_tokenFor a linked patient, the stable tk_… token for future token uploads, else null
skip_pseudonyms[]Always present — all currently rejected pseudonyms of the practice; the client caches them and skips them on the next export (identical to GET /rejected-clients)
warnings[]e.g. rejected_pseudonym, resend_with_hints, stale_pseudonym

Duplicate semantics

An import is a duplicate when the same logical measurement already exists. Server-side via INSERT … ON CONFLICT DO NOTHING/UPDATE on the natural key (account_id, device_pseudonym, eye, measured_at in the pending queue, or measurement uniques on the linked patient). duplicate: true only means “already present” — the status of the existing row (even rejected) is left untouched.

devicePseudonym — the patient identifier

  • A stable, client-computed hash per patient (in the reference implementation: 128-bit truncated HMAC-SHA256 → 32 hex chars). The server does not compute it and validates only the format (^[0-9a-f]{32}$).
  • For the OEM integration it is an opaque, stable key: same patient + same client ⇒ same pseudonym. Your backend treats it, per practice (account_id), as a unique patient key — it needs to know nothing more about it.
  • It is unique per practice (tenant-scoped), not global.