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:
- A standard code. LOINC identifies the analyte, the specimen and the method:
2345-7is glucose in serum or plasma,2339-0is glucose in whole blood. The lab's own code can travel next to it. - A value with a unit. Ideally a UCUM unit (
mg/dL,mmol/L,10*3/uL), so values from two labs can be converted and compared. - The reference range the lab used, and an interpretation flag (H, L, N…) when the lab gives one.
- Status: preliminary, final, corrected or amended. Corrections happen; an API must let you see the new version without losing the old one.
- Times: when the specimen was collected (
effectiveDateTime) and when the result was released (issued). - Origin: which laboratory produced it and which report or order it belongs to.
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:
- FHIR directly:
POST /fhir/R4/Observation, or a transaction Bundle with theDiagnosticReportand its Observations together. - Events: a
resource.createdwebhook per Observation, so a clinic or an app can react when results arrive. - For people and AI: the patient sees the results in their app; applications they consent to read them;
POST /v1/contextgives an AI model a source-labelled summary that includes recent lab values. - Provenance: every imported result records
author_type = import, the source system and the report id (medical data provenance).
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
- Does it store results as FHIR R4 Observations with LOINC codes, or as rows in its own schema?
- Can it accept the formats your labs actually send (HL7 v2 in most hospitals, CSV or PDF from smaller labs)?
- What happens to a marker it cannot map: is it rejected, dropped or kept with the lab's code?
- Are retries safe, or does a resent report create duplicates?
- Are corrected results versioned, with the earlier value still readable?
- Is the origin of every value recorded and visible to the patient?
- Who can read the results, and is that decided by the patient's consent?
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
- How to import laboratory results into a FHIR record: the step-by-step tutorial
- Lab connector: the endpoint reference
- How to normalize medical data: LOINC, UCUM, dates, ranges and duplicates
- Medical data provenance explained
- FHIR code systems
- How to integrate clinic systems with a FHIR backend
- FHIR vs HL7 v2: practical differences