A lab report API should return each test as its own object holding the printed test name, the result, its units, the reference range, the abnormal flag as printed, the specimen and the collection and result dates. Results need room for numbers, text and comparators, and amended results need a status that shows they changed.
Lab report API fields per test result
A lab report API returns one object per test, and every object needs the same core fields: the test name as printed, the result, the units, the reference range, the flag, the specimen type, and the collection and result dates. Report-level facts such as the patient, ordering provider and performing lab sit once, above the tests.
The per-test fields break down as follows.
| Field | What it holds | Example value (fictional) |
|---|---|---|
test_name_printed | The name exactly as the lab printed it | "Glucose, Serum" |
result | The value, typed by kind (see the next section) | 128 |
units | The unit as printed | "mg/dL" |
reference_range | Low, high and the printed text | 70 to 99, "70-99" |
flag_printed | The abnormal flag exactly as printed, or null | "H" |
specimen | The specimen type when printed | "Serum" |
status | Whether the result is final, preliminary or changed | "final" |
collected_at and resulted_at | When the sample was taken and when the result was released | "2026-10-02T07:40:00" |
confidence and page | How sure the extractor was, and where the value sits | 0.94, page 1 |
Report-level fields include the patient name and date of birth, the ordering provider, the performing lab's name, the accession or specimen number, and the report date. The accession number is the most useful key when the same lab sends a second version of a report.
Keep the flag as printed. Labs print flags differently, as letters, symbols or bold type, and a lab report API is not the place to reinterpret them. How labs print flags is covered in how abnormal flags are printed on lab reports. This post covers the data shape only and never what a value means.
The pipeline that produces these fields, from intake to review, is laid out in lab report data extraction. The confidence and page fields follow the same pattern as other document types, which the EOB API post also uses for claims.
Schema Care reads lab reports into test names, results, units, reference ranges and abnormal flags, with a confidence score and a page link for each, and it queues uncertain values for a person to check. Schema Care offers no API, SDK, webhooks, FHIR output, 835 output or 837 output; the lab report fields it reads are listed with the other document types on the Schema Care homepage.
How should a lab API represent numeric, text and range results?
A lab API should represent every result as a small object with a kind and the parts that kind needs, rather than forcing all results into one number or one string. A numeric result has a value and a unit. A text result has text. A result like "<0.5" has a comparator and a value.
The FHIR Observation resource shows one workable design. The HL7 FHIR R4 Observation page allows a result, value[x], to be a Quantity, a CodeableConcept, a string, a boolean, an integer, a Range, a Ratio and several other types. Most lab results fit three of them: Quantity for numbers, CodeableConcept or string for text, and Range for a result reported as an interval.
Comparators handle results printed with a sign. The HL7 FHIR R4 quantity comparator value set defines four codes: <, <=, >= and >. A result printed as "<0.5" becomes a value of 0.5 with the comparator <. Storing it as the number 0.5 alone would change what the lab reported.
Reference ranges need the same care. Most print as a low and a high, such as "3.5-5.1", but some print as "<200", "Negative" or a table by age. The Observation page allows referenceRange to hold low, high and text, and says text may be used when a quantitative range is not appropriate, giving "Negative" as an example. A lab report API should always return the printed range text and add parsed low and high values only when the parse is clean.
A practical result object looks like this:
- numeric:
{ "kind": "numeric", "value": 128, "units": "mg/dL" } - comparator:
{ "kind": "numeric", "comparator": ">", "value": 60, "units": "mL/min/1.73m2" } - text:
{ "kind": "text", "text": "Positive" } - not reported:
{ "kind": "absent", "reason": "not_performed", "text": "Test not performed" }
The absent case matters. The HL7 FHIR R4 data absent reason value set includes not-performed for a test that was not done and error for a system or workflow failure. A blank result is not the same as a zero, and a lab report API should never return one where the other was printed. Pulling ranges off the page is covered in depth in extracting reference ranges from lab reports.
Should a lab report API return LOINC codes?
A lab report API should return a LOINC code when it can map one confidently, and it should always return the printed test name beside it. LOINC is a standard code system for laboratory tests and observations. A code makes results comparable across labs; the printed name keeps the record faithful to the page.
The NLM page on LOINC describes it as "a clinical terminology important for laboratory test orders and results," produced by the Regenstrief Institute. The value of a code is that "Glucose, Serum", "GLU" and "Glucose, fasting" from three different labs can be compared once each is mapped, and that matters for trending and quality reporting.
Mapping is where errors creep in. Two tests with similar names can differ in specimen, method or timing, and each difference can mean a different code. An API that guesses a code from the name alone will sometimes be wrong with full confidence. A good lab report API returns the code with its own confidence, separate from the confidence in the printed value, and leaves the code empty when it is unsure.
Keep the printed name for three reasons. Reviewers check values against the page, and the page shows the lab's name, not a code. Codes are sometimes remapped as the code table is updated, and the printed name lets you redo the mapping. And a consumer that does not use LOINC still needs a readable name.
The HL7 FHIR R4 Observation page binds Observation.code to LOINC only at example strength, which means FHIR itself does not require LOINC there. Whether your EHR or registry does is a separate question to ask it. How to map names to codes step by step, and the mistakes that come from matching the name but not the specimen, are covered in mapping lab test names to LOINC.
Lab report API edge cases: panels, amended results and multi-page reports
Lab report API edge cases are the report layouts that break a simple "one row, one test" parser: panels that group many tests under one order, results the lab changed after release, and reports whose tests or comments run onto a second page. Each needs a field in the response that a naive design leaves out.
Panels. A basic metabolic panel prints as one order with several tests beneath it. The HL7 FHIR R4 Observation page defines hasMember for a group observation such as "a battery, a panel of tests." A lab report API should return the panel as a parent with its tests as children, so a consumer can tell that sodium and potassium came from the same draw. Panel layouts on the page are covered in extracting panel results like a CBC or CMP.
Amended results. A lab may change a result after release. The HL7 FHIR R4 observation status value set defines amended as a result modified after being final, and corrected as an amendment made to fix an error. The HL7 FHIR R4 diagnostic report status value set adds appended for new content added without changing what was there. A lab report API should return the status as printed and, when the report shows the earlier value, return that too.
Multi-page reports. A test list can break across pages, and so can a comment that belongs to the last test on page 1. Every value needs its own page number, and comments should attach to the test they follow rather than to the page they sit on.
Cumulative reports. Some reports print several dates of the same test side by side. The HL7 FHIR R4 DiagnosticReport page says that resource is not meant for cumulative result tables. Each date's value should become its own result with its own collection date.
Sample lab report API response
The response below is an example only, for a fictional basic metabolic panel. It comes from the placeholder address https://api.example.com. The patient, accession number, dates and values were made up for this post and describe no real person. Schema Care does not offer an API, and this is not its output. Only four of the panel's tests are shown, to keep the example short.
GET https://api.example.com/v1/lab-reports/lab_example_0907
{
"report_id": "lab_example_0907",
"patient_name": { "value": "Theo Marchetti-Ward", "confidence": 0.97, "page": 1 },
"accession_number": { "value": "EX-ACC-260907-118", "confidence": 0.95, "page": 1 },
"performing_lab": { "value": "Example Reference Laboratory", "confidence": 0.98, "page": 1 },
"report_status": { "value": "corrected", "confidence": 0.93, "page": 1 },
"panels": [
{
"panel_name_printed": "Basic Metabolic Panel",
"loinc": null,
"collected_at": "2026-10-02T07:40:00",
"specimen": "Serum",
"tests": [
{
"test_name_printed": "Glucose",
"result": { "kind": "numeric", "value": 128, "units": "mg/dL" },
"reference_range": { "text": "70-99", "low": 70, "high": 99 },
"flag_printed": "H",
"status": "final",
"confidence": 0.95,
"page": 1
},
{
"test_name_printed": "Potassium",
"result": { "kind": "text", "text": "See comment" },
"reference_range": { "text": "3.5-5.1", "low": 3.5, "high": 5.1 },
"flag_printed": null,
"status": "final",
"comment": "Example comment: specimen hemolyzed, recollection requested.",
"confidence": 0.86,
"needs_review": true,
"page": 1
},
{
"test_name_printed": "Creatinine",
"result": { "kind": "numeric", "value": 0.9, "units": "mg/dL" },
"previous_result": { "kind": "numeric", "value": 1.9, "units": "mg/dL" },
"reference_range": { "text": "0.6-1.2", "low": 0.6, "high": 1.2 },
"flag_printed": null,
"status": "corrected",
"confidence": 0.91,
"page": 2
},
{
"test_name_printed": "eGFR",
"result": { "kind": "numeric", "comparator": ">", "value": 60, "units": "mL/min/1.73m2" },
"reference_range": { "text": ">59", "low": null, "high": null },
"flag_printed": null,
"status": "final",
"confidence": 0.9,
"page": 2
}
]
}
]
}
The sample shows the four cases the design must hold. Glucose is a plain numeric result with a printed flag. Potassium is a text result with a comment and falls under an example review threshold. Creatinine is a corrected result that keeps the earlier value. eGFR is a comparator result whose range has no parsed limits, so only the text survives. None of this says anything about what the values mean.
Delivering lab report API output to an EHR
Lab report API output usually ends up in an EHR, and the route depends on what the EHR accepts: an HL7 version 2 results interface, a FHIR API, or a person entering results into a form. The JSON response is an intermediate step, not the final home for the data.
An HL7 version 2 results interface receives lab results as ORU messages, and the receiving side expects value, units, range and flag as separate pieces rather than one line of text. That is one more reason a lab report API should keep those fields apart. What an ORU message carries for each result, how extracted fields map onto it, and when an interface is worth building at all are covered in HL7 ORU messages vs PDF lab reports.
A FHIR API expects a DiagnosticReport that references one Observation per test. The HL7 FHIR R4 DiagnosticReport page gives the report a result element that references Observations and a presentedForm element for the report as it was presented, which is where the original PDF can travel. How the report groups its Observations is covered in how DiagnosticReport groups lab observations. The billing counterpart of this design, the FHIR ExplanationOfBenefit resource, follows the same pattern of one parent resource with line-level detail.
Manual entry is still common for outside labs. In that case, the API output feeds a review screen where a person confirms each value against the page before typing or pasting it into the EHR. The confidence score and page link do the most work here, because they tell the person which values to look at first.
Whatever the route, three rules hold. Send only reviewed values to the chart. Keep the link from each value to the page it came from. And never drop a status of corrected or amended, because a downstream system that sees only the new value cannot tell the record changed. For the full set of questions to ask any extraction endpoint, see what a medical document extraction API should do.