AnpherosAnpheros PlatformDevelopers

Lab results API: HL7 v2, FHIR and LOINC

How lab results travel as HL7 v2 and FHIR R4 data, what one result must carry, and how to receive lab reports into a FHIR record, with a real example.

A laboratory results API delivers lab results as structured data — test code, value, unit, reference range, status and time — instead of a PDF, so software can store, compare, chart and share them. Today that data travels in three forms: HL7 v2 ORU^R01 messages (still the backbone between labs and hospitals), HL7 FHIR R4 Observation and DiagnosticReport resources (the format of modern APIs), and lab-specific CSV, JSON or PDF exports. This page explains how the pieces fit, what a good lab results API must carry, and how to receive lab data into a patient's FHIR record with Anpheros.

Who exchanges lab results, and in which format

From → to Usual format today Notes
Laboratory → hospital or clinic system HL7 v2 ORU^R01 (in the US often per the HL7 v2.5.1 Laboratory Results Interface guide) Orders go the other way as ORM/OML messages
Laboratory → public health agency HL7 v2 electronic lab reporting, increasingly FHIR Reportable conditions only
Clinic system → patient app FHIR R4 through a patient-access API In the US, certified EHRs expose lab results as FHIR (US Core); in the EU the European Health Data Space lists laboratory results among its priority data categories
Laboratory → patient PDF report, portal, sometimes CSV The format most patients actually receive
App → AI model Structured context built from Observations Needs codes and units to be reliable

The direction matters: a laboratory information system (LIS) is the source of truth for a result, and every system downstream keeps a copy. What makes the copies trustworthy is that each one keeps the code, the unit, the range and the origin of the value.

What one lab result needs to carry

A result that only says "Glucose 126" cannot be interpreted safely. A complete result has:

In FHIR R4 each result is one Observation with category = laboratory, and the report that groups them is a DiagnosticReport that lists the Observations in result. Valid examples of both, with the mistakes validators report most often, are in the FHIR R4 JSON examples — see also the DiagnosticReport example.

HL7 v2 and FHIR side by side

The same lipid panel result, as most labs send it today and as an API should store it:

OBX|1|NM|2093-3^Cholesterol^LN||212|mg/dL|<200|H|||F|||20261002074500
HL7 v2 field Meaning FHIR R4 Observation element
OBX-3 2093-3^Cholesterol^LN Test, coded in LOINC code.coding (system http://loinc.org)
OBX-5 212, OBX-2 NM Numeric value valueQuantity.value
OBX-6 mg/dL Unit valueQuantity.unit
OBX-7 <200 Reference range referenceRange.high
OBX-8 H Abnormal flag interpretation
OBX-11 F Final result status = final
OBX-14 Observation time effectiveDateTime

Converting between the two is mostly field mapping. The hard parts are elsewhere: labs that send their own codes instead of LOINC, numbers written as text ("6,4", "<5"), ranges in several notations, and reports that are sent again after a timeout. How to normalize medical data covers those in detail.

Receiving lab results into a FHIR record with Anpheros

Anpheros stores each patient's record as HL7 FHIR R4 and has a lab connector: one request takes a whole report as HL7 v2, CSV or JSON and returns laboratory Observations with LOINC codes, reference ranges, interpretation and provenance.

curl -s -X POST "https://platform.anpheros.com/v1/patients/$PID/labs/import" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: x-application/hl7-v2+er7" \
  -H "X-Anpheros-Source-System: springfield-lis" \
  --data-binary @lipid-panel.hl7

For a three-line lipid panel (cholesterol, HDL, triglyceride) the answer, from a local run of the platform, is:

{
  "created": 3, "skipped": 0, "unmapped": [], "warnings": [],
  "observations": [
    {"id": "26ed9219-…", "marker": "Cholesterol", "code": "2093-3", "system": "http://loinc.org",
     "value": 212.0, "unit": "mg/dL", "date": "2026-10-02T07:45:00Z"},
    {"id": "2633d1d4-…", "marker": "HDL cholesterol", "code": "2085-9", "system": "http://loinc.org",
     "value": 48.0, "unit": "mg/dL", "date": "2026-10-02T07:45:00Z"},
    {"id": "a02520bb-…", "marker": "Triglyceride", "code": "2571-8", "system": "http://loinc.org",
     "value": 165.0, "unit": "mg/dL", "date": "2026-10-02T07:45:00Z"}
  ]
}

Each stored result is a standard FHIR R4 Observation, readable by any FHIR client:

{
  "resourceType": "Observation",
  "status": "final",
  "category": [{"coding": [{"system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "laboratory"}]}],
  "code": {"coding": [{"system": "http://loinc.org", "code": "2093-3",
                        "display": "Cholesterol [Mass/volume] in Serum or Plasma"}], "text": "Cholesterol"},
  "subject": {"reference": "Patient/4c3062d9-…"},
  "effectiveDateTime": "2026-10-02T07:45:00Z",
  "valueQuantity": {"value": 212.0, "unit": "mg/dL"},
  "interpretation": [{"coding": [{"system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", "code": "H"}]}],
  "referenceRange": [{"high": {"value": 200.0, "unit": "mg/dL"}}]
}

Sending the same message again creates nothing new ("created": 0, "skipped": 3): every row gets a stable identifier from the report id, code, date and value. Markers the platform cannot map to LOINC keep the lab's code and come back in unmapped, so you can fix the mapping at the source.

Reading results back is a standard FHIR search:

curl -s "https://platform.anpheros.com/fhir/R4/Observation?patient=$PID&category=laboratory&code=http://loinc.org|2093-3" \
  -H "Authorization: Bearer $KEY"

Other ways in and out of the same data:

The full walk-through, with a CSV report, an HL7 v2 message, unmapped markers and resent reports on a synthetic patient, is the tutorial How to import laboratory results into a FHIR record.

Check results before you send them

The free FHIR R4 validator checks an Observation's structure and required value sets, and with the eu-lab profile it also checks what a laboratory result should carry: the laboratory category, a LOINC code, a subject, a final-type status, a time, a value, a UCUM unit and a reference range. The same checks run on the platform through POST /fhir/R4/Observation/$validate?profile=eu-lab.

Choosing a lab results API: a checklist

What Anpheros does not do

Anpheros is not a laboratory information system and is not connected to commercial laboratory networks: the laboratory, the clinic or the patient's app sends the report. It does not route lab orders, does not read PDF reports through this API (the Anpheros Daily app extracts results from photos and PDFs on the patient's side), and does not interpret results clinically. Units are stored as the laboratory sends them; converting them to UCUM codes is on the sender, as described in How to normalize medical data.

Frequently asked questions

What is a lab results API?

An interface that lets software send or receive laboratory results as structured data — a coded test, a value with a unit, a reference range, a status and a time — rather than as a document. Modern lab results APIs use HL7 FHIR R4 Observation and DiagnosticReport resources; most laboratories still emit HL7 v2 ORU^R01 messages.

Which code system identifies lab tests?

LOINC. Each LOINC code names the analyte, the specimen and often the method, so 2345-7 (glucose in serum or plasma) and 2339-0 (glucose in whole blood) are different results. Units are best expressed in UCUM.

What is the difference between an Observation and a DiagnosticReport?

An Observation is one result, such as cholesterol 212 mg/dL. A DiagnosticReport is the report as a whole — the panel that was ordered, who issued it and when — and lists its Observations in result.

Can I import HL7 v2 lab messages into a FHIR server?

Yes. Each OBX segment maps to one Observation (OBX-3 to code, OBX-5 and OBX-6 to valueQuantity, OBX-7 to referenceRange, OBX-8 to interpretation). The Anpheros lab connector does this in one request and keeps the lab's report id in Provenance.

How do I avoid duplicate lab results when a report is resent?

Give every result a stable identifier derived from the report, the test, the time and the value, and skip rows whose identifier already exists. The Anpheros lab connector does this automatically and reports the count as skipped.

Related