SpectraDx
pectraDx
SpectraDxlight to insight
← Back to blog
Integration

HL7v2 Spectroscopy Integration: Generating ORU^R01 Messages

HL7 integration for spectroscopy instruments: generate ORU^R01 messages from spectral classification results that Epic, Cerner, and other EHRs accept.

HL7v2 Spectroscopy Integration: Generating ORU^R01 Messages

Your spectroscopy classifier works. It takes a raw spectrum, runs it through your model, and returns "Strep A Positive" with 97.3% confidence. Ship it, right?

Not even close. That classification result is sitting on your instrument workstation. The patient's chart is in Epic. The ordering physician is waiting on results in Cerner. The billing system needs a finalized result to trigger reimbursement. None of these systems know your spectrometer exists, and none of them will ever speak your instrument's native protocol.

The gap between "working classifier" and "clinically useful result" is an HL7 message. Specifically, an ORU^R01 - the observation result message that every laboratory information system in the United States has understood since the early 1990s.

This article walks through how to generate properly formatted ORU^R01 messages from spectral classification results, with working Python code and the field-level detail you need to get your messages accepted by real EHR systems.

Why HL7v2 and not FHIR

Let's address the obvious question first. FHIR (Fast Healthcare Interoperability Resources) is the modern standard. It uses JSON over REST. It has a well-designed resource model. It is, by every technical measure, superior to HL7v2.

It also does not matter for instrument integration in 2026.

Here is the reality on the ground: over 95% of US hospital laboratory information systems still use HL7v2 for instrument-to-LIS communication. The HL7v2 interface engine - typically Rhapsody, Mirth Connect, or an Epic Bridges instance - is the backbone of every hospital's lab data flow. These engines receive ORU^R01 messages from analyzers, blood gas machines, hematology instruments, and urinalysis systems. They have done so for decades.

FHIR adoption in the lab space is growing, but it is concentrated in EHR-to-EHR communication, patient portals, and third-party app access. The instrument-to-LIS channel remains overwhelmingly HL7v2. When you connect a new analyzer to a hospital's LIS, the integration team will hand you an HL7v2 interface specification document. Not a FHIR capability statement.

The pragmatic approach: build HL7v2 first, add FHIR as a secondary output format. We cover the FHIR DiagnosticReport structure at the end of this article, but the core implementation is HL7v2.

The ORU^R01 message structure

HL7v2 messages are pipe-delimited text. Each line is a segment, identified by a three-character code. Fields within a segment are separated by |, components within a field by ^, and sub-components by &. The encoding characters are defined in the MSH segment header, but virtually every implementation uses the defaults.

An ORU^R01 message for a spectral classification result needs five segments at minimum, six in practice:

SegmentNamePurpose
MSHMessage HeaderIdentifies the sending system, receiving system, message type, and encoding rules
PIDPatient IdentificationThe patient's MRN, name, date of birth, and sex - links the result to the correct chart
PV1Patient VisitThe encounter context - which visit this result belongs to. Critical for inpatient settings
OBRObservation RequestThe test that was ordered - order number, specimen information, ordering provider
OBXObservation ResultThe actual result. You will typically send multiple OBX segments: one for the classification result, one for the confidence score, and optionally one for spectral quality

Here is a complete, properly formatted ORU^R01 message for an FTIR-based Strep A classification result:

MSH|^~\&|SPECTRADX|MAIN_LAB|EPIC|HOSPITAL|20260508143022||ORU^R01^ORU_R01|MSG00001|P|2.5.1|||AL|NE||UNICODE UTF-8
PID|1||MRN001234^^^HOSPITAL^MR||DOE^JANE^M||19850315|F|||123 MAIN ST^^SPRINGFIELD^IL^62704
PV1|1|O|CLINIC_A^^^^||||1234567890^SMITH^ROBERT^J^^^MD|||||||||V123456^^^HOSPITAL^VN
OBR|1|ORD98765^SPECTRADX|SPE20260508001^SPECTRADX|87880^Strep A direct antigen^CPT|||20260508142500|||||||20260508142800|THROAT^Throat swab|1234567890^SMITH^ROBERT^J^^^MD||||||20260508143022|||F
OBX|1|CE|6558-6^Streptococcus pyogenes Ag Throat^LN||10828004^Positive^SCT|||A|||F|||20260508143022
OBX|2|NM|SPECTRADX_CONF^Classification Confidence^L||97.3|%|95.0|N|||F|||20260508143022
OBX|3|NM|SPECTRADX_SNR^Spectral SNR^L||42.8||20.0|N|||F|||20260508143022

Let's break down every segment in detail.

MSH: Message Header

MSH|^~\&|SPECTRADX|MAIN_LAB|EPIC|HOSPITAL|20260508143022||ORU^R01^ORU_R01|MSG00001|P|2.5.1|||AL|NE||UNICODE UTF-8
FieldPositionValueMeaning
Encoding charactersMSH-2^~\&Component, repetition, escape, sub-component separators
Sending applicationMSH-3SPECTRADXYour instrument software identifier
Sending facilityMSH-4MAIN_LABThe lab where the instrument sits
Receiving applicationMSH-5EPICThe target LIS/EHR
Receiving facilityMSH-6HOSPITALThe institution
Message timestampMSH-720260508143022yyyyMMddHHmmss format
Message typeMSH-9ORU^R01^ORU_R01Observation result, unsolicited
Message control IDMSH-10MSG00001Unique per message - must never repeat
Processing IDMSH-11PP = production, T = training, D = debugging
VersionMSH-122.5.1HL7 version - check what your LIS expects

The message control ID (MSH-10) is critical. Every message you send must have a globally unique control ID. The receiving system uses this for deduplication and acknowledgment tracking. Use a UUID or a monotonically increasing sequence number with a system prefix.

PID: Patient Identification

PID|1||MRN001234^^^HOSPITAL^MR||DOE^JANE^M||19850315|F|||123 MAIN ST^^SPRINGFIELD^IL^62704

The MRN in PID-3 is a composite field: the ID number, then three empty components, then the assigning authority (which hospital system issued this MRN), then the identifier type code (MR for medical record number). Getting this wrong is the single most common cause of message rejection. The assigning authority must match exactly what the receiving system expects.

Patient name in PID-5 follows the format: LAST^FIRST^MIDDLE. Date of birth in PID-7 is yyyyMMdd. Sex in PID-8 uses the HL7 administrative sex table: F, M, O (other), U (unknown).

PV1: Patient Visit

PV1|1|O|CLINIC_A^^^^||||1234567890^SMITH^ROBERT^J^^^MD|||||||||V123456^^^HOSPITAL^VN

PV1-2 is the patient class: I (inpatient), O (outpatient), E (emergency). PV1-3 is the patient location. PV1-7 is the attending physician with their NPI. PV1-19 is the visit number - this ties the result to a specific encounter.

For point-of-care spectroscopy, you will usually be sending outpatient results. The visit number comes from the order that triggered the test.

OBR: Observation Request

OBR|1|ORD98765^SPECTRADX|SPE20260508001^SPECTRADX|87880^Strep A direct antigen^CPT|||20260508142500|||||||20260508142800|THROAT^Throat swab|1234567890^SMITH^ROBERT^J^^^MD||||||20260508143022|||F
FieldPositionValueMeaning
Placer order numberOBR-2ORD98765^SPECTRADXThe order number from the ordering system
Filler order numberOBR-3SPE20260508001^SPECTRADXYour instrument's accession number
Universal service IDOBR-487880^Strep A direct antigen^CPTCPT code for the test
Observation date/timeOBR-720260508142500When the specimen was collected
Specimen receivedOBR-1420260508142800When the instrument received the specimen
Specimen sourceOBR-15THROAT^Throat swabSpecimen type and source
Ordering providerOBR-16NPI and nameWho ordered the test
Results date/timeOBR-2220260508143022When results were finalized
Result statusOBR-25FF = final, P = preliminary, C = corrected

The placer order number (OBR-2) comes from the LIS - it is the order number assigned when the physician ordered the test. The filler order number (OBR-3) is yours to assign. Together, these two numbers create the bidirectional link between the order and the result.

OBX: Observation Result - the critical segment

This is where your spectral classification result lives. Each OBX segment carries one discrete observation.

OBX|1|CE|6558-6^Streptococcus pyogenes Ag Throat^LN||10828004^Positive^SCT|||A|||F|||20260508143022
FieldPositionValueMeaning
Set IDOBX-11Sequential counter within this group
Value typeOBX-2CECoded entry - the result is a coded value
Observation identifierOBX-36558-6^Streptococcus pyogenes Ag Throat^LNLOINC code identifying what was measured
Observation valueOBX-510828004^Positive^SCTSNOMED CT coded result
Abnormal flagsOBX-8AA = abnormal (positive finding)
Result statusOBX-11FF = final
Observation dateOBX-1420260508143022When this observation was produced

OBX-2 (Value Type) determines how OBX-5 is interpreted:

CodeTypeWhen to use
CECoded EntryCategorical results: Positive, Negative, Indeterminate. Value is a code from a standard terminology (SNOMED CT, local codes)
NMNumericConfidence scores, SNR values, concentration measurements
STStringFree-text results or comments. Avoid when a coded value is available
TXTextLonger narrative observations

OBX-3 (Observation Identifier) should use LOINC codes whenever possible. For microbiology identification results, common LOINC codes include:

LOINC CodeDescription
6558-6Streptococcus pyogenes Ag, Throat
5081-0Bacterial identification, Culture
634-6Bacteria identified in specimen by Culture

For novel spectroscopy-based tests that do not have established LOINC codes, use a local code with a L coding system designator: SPECTRADX_FTIR_001^FTIR Bacterial Classification^L. Work with your hospital's lab director to request new LOINC codes through Regenstrief.

OBX-8 (Abnormal Flags) values relevant to spectroscopy results:

  • N - Normal (e.g., negative result, confidence within expected range)
  • A - Abnormal (e.g., positive pathogen detection)
  • H - High (e.g., confidence score above reference range - less common)
  • Empty - No abnormality assessment

Confidence score as a separate OBX

OBX|2|NM|SPECTRADX_CONF^Classification Confidence^L||97.3|%|95.0|N|||F|||20260508143022

The confidence score goes in its own OBX segment with value type NM (numeric). OBX-6 carries the units (%), and OBX-7 carries the reference range. Setting the reference range to your model's validation threshold (e.g., 95.0) lets the LIS flag low-confidence results automatically.

This is important: if your classifier returns a result below your confidence threshold, you should either suppress the result entirely and flag it for manual review, or send it with result status P (preliminary) instead of F (final). Sending a low-confidence result as final is a patient safety issue.

Building ORU^R01 messages in Python

Here is a working implementation that generates ORU^R01 messages from spectral classification results. This uses manual string construction rather than a library dependency, because in production you want full control over every field.

from datetime import datetime
import uuid
 
 
def generate_message_control_id() -> str:
    """Generate a unique message control ID."""
    return f"SDX{uuid.uuid4().hex[:12].upper()}"
 
 
def format_hl7_datetime(dt: datetime = None) -> str:
    """Format a datetime as HL7v2 timestamp (yyyyMMddHHmmss)."""
    if dt is None:
        dt = datetime.now()
    return dt.strftime("%Y%m%d%H%M%S")
 
 
def generate_oru_r01(
    patient_mrn: str,
    patient_name: tuple[str, str, str],  # (last, first, middle)
    patient_dob: str,  # yyyyMMdd
    patient_sex: str,  # F, M, O, U
    visit_number: str,
    ordering_provider: tuple[str, str, str, str],  # (NPI, last, first, mid)
    placer_order_number: str,
    test_cpt: str,
    test_name: str,
    specimen_source: str,
    specimen_collected: datetime,
    result_loinc: str,
    result_loinc_name: str,
    result_code: str,
    result_display: str,
    result_code_system: str,  # SCT, LN, L, etc.
    abnormal_flag: str,  # N, A, H, or empty
    confidence: float,
    confidence_threshold: float,
    spectral_snr: float = None,
    snr_threshold: float = None,
    sending_app: str = "SPECTRADX",
    sending_facility: str = "MAIN_LAB",
    receiving_app: str = "LIS",
    receiving_facility: str = "HOSPITAL",
    assigning_authority: str = "HOSPITAL",
) -> str:
    """
    Generate an HL7v2 ORU^R01 message from a spectral classification result.
 
    Returns the complete message as a string with \\r segment terminators
    (per HL7v2 spec - many systems also accept \\n).
    """
    now = datetime.now()
    msg_control_id = generate_message_control_id()
    result_time = format_hl7_datetime(now)
    specimen_time = format_hl7_datetime(specimen_collected)
 
    # Determine result status based on confidence
    result_status = "F" if confidence >= confidence_threshold else "P"
 
    # Generate filler order number (our accession number)
    filler_order = f"SPE{now.strftime('%Y%m%d')}{uuid.uuid4().hex[:4].upper()}"
 
    last, first, middle = patient_name
    prov_npi, prov_last, prov_first, prov_mid = ordering_provider
 
    segments = []
 
    # MSH - Message Header
    segments.append(
        f"MSH|^~\\&|{sending_app}|{sending_facility}"
        f"|{receiving_app}|{receiving_facility}"
        f"|{format_hl7_datetime(now)}"
        f"||ORU^R01^ORU_R01"
        f"|{msg_control_id}|P|2.5.1|||AL|NE||UNICODE UTF-8"
    )
 
    # PID - Patient Identification
    segments.append(
        f"PID|1||{patient_mrn}^^^{assigning_authority}^MR"
        f"||{last}^{first}^{middle}"
        f"||{patient_dob}|{patient_sex}"
    )
 
    # PV1 - Patient Visit
    segments.append(
        f"PV1|1|O|^^^^"
        f"||||{prov_npi}^{prov_last}^{prov_first}^{prov_mid}^^^MD"
        f"|||||||||{visit_number}^^^{assigning_authority}^VN"
    )
 
    # OBR - Observation Request
    segments.append(
        f"OBR|1|{placer_order_number}^{sending_app}"
        f"|{filler_order}^{sending_app}"
        f"|{test_cpt}^{test_name}^CPT"
        f"|||{specimen_time}"
        f"|||||||{specimen_time}"
        f"|{specimen_source}"
        f"|{prov_npi}^{prov_last}^{prov_first}^{prov_mid}^^^MD"
        f"||||||{result_time}|||{result_status}"
    )
 
    # OBX-1 - Classification result (coded entry)
    segments.append(
        f"OBX|1|CE"
        f"|{result_loinc}^{result_loinc_name}^LN"
        f"||{result_code}^{result_display}^{result_code_system}"
        f"|||{abnormal_flag}|||{result_status}|||{result_time}"
    )
 
    # OBX-2 - Confidence score (numeric)
    conf_flag = "N" if confidence >= confidence_threshold else "L"
    segments.append(
        f"OBX|2|NM"
        f"|SPECTRADX_CONF^Classification Confidence^L"
        f"||{confidence:.1f}|%|{confidence_threshold:.1f}"
        f"|{conf_flag}|||{result_status}|||{result_time}"
    )
 
    # OBX-3 - Spectral SNR (numeric, optional)
    if spectral_snr is not None:
        snr_flag = "N" if snr_threshold is None or spectral_snr >= snr_threshold else "L"
        snr_ref = f"|{snr_threshold:.1f}" if snr_threshold else "|"
        segments.append(
            f"OBX|3|NM"
            f"|SPECTRADX_SNR^Spectral SNR^L"
            f"||{spectral_snr:.1f}|"
            f"{snr_ref}"
            f"|{snr_flag}|||{result_status}|||{result_time}"
        )
 
    # Join with carriage return (HL7v2 segment terminator)
    return "\r".join(segments) + "\r"

Usage example:

from datetime import datetime
 
message = generate_oru_r01(
    patient_mrn="MRN001234",
    patient_name=("DOE", "JANE", "M"),
    patient_dob="19850315",
    patient_sex="F",
    visit_number="V123456",
    ordering_provider=("1234567890", "SMITH", "ROBERT", "J"),
    placer_order_number="ORD98765",
    test_cpt="87880",
    test_name="Strep A direct antigen",
    specimen_source="THROAT^Throat swab",
    specimen_collected=datetime(2026, 5, 8, 14, 25, 0),
    result_loinc="6558-6",
    result_loinc_name="Streptococcus pyogenes Ag Throat",
    result_code="10828004",
    result_display="Positive",
    result_code_system="SCT",
    abnormal_flag="A",
    confidence=97.3,
    confidence_threshold=95.0,
    spectral_snr=42.8,
    snr_threshold=20.0,
    receiving_app="EPIC",
    receiving_facility="HOSPITAL",
)
 
print(message)

A few implementation notes worth emphasizing:

Segment terminators. The HL7v2 specification mandates \r (carriage return, 0x0D) as the segment terminator. Not \n, not \r\n. Many interface engines are tolerant of \n, but some are not. Use \r and you will never have a problem.

Character encoding. HL7v2 traditionally used ASCII. Modern implementations should declare UTF-8 in MSH-18 (UNICODE UTF-8). Verify with your receiving system - some older LIS installations do not handle multi-byte characters correctly in patient names.

Message control ID uniqueness. The implementation above uses a prefix plus a truncated UUID. In production, consider using a persistent counter backed by your database. The control ID must be unique across the lifetime of the interface, not just per session.

Handling result corrections and amendments

When a spectral result needs to be corrected - perhaps after manual review overrides the classifier - you send a new ORU^R01 with OBR-25 set to C (corrected) instead of F (final). The OBR-2 (placer order number) and OBR-3 (filler order number) must match the original message so the LIS can link the correction to the original result.

# For a corrected result, change result_status and resend
# OBR-25 = "C" (corrected)
# Include OBX with updated values
# Same placer and filler order numbers as the original

The LIS will overwrite the previous result and flag it as corrected in the patient's chart. This audit trail is an FDA requirement for clinical diagnostic devices.

Testing your HL7 messages

Before you connect to a hospital's production interface engine, you need to validate your messages. Here is the testing progression we follow:

1. Structural validation. Use the HAPI HL7v2 test panel or the Mirth Connect message template validator. These tools parse your message and flag structural errors: missing required fields, invalid data types, segment ordering violations. The HAPI test panel is free, Java-based, and handles all HL7v2 versions.

2. Vocabulary validation. Verify that your LOINC codes, SNOMED CT codes, and CPT codes are valid and current. The NLM's LOINC search (https://loinc.org/search/) and the SNOMED CT browser let you confirm code validity. Using an expired or incorrect code will cause silent data quality issues - the message will be accepted, but the result will be miscategorized.

3. Integration testing with a test LIS. Most EHR vendors provide sandbox environments:

  • Epic has the Epic Open Sandbox with HL7v2 interface testing capabilities
  • Oracle Health (Cerner) provides Cerner Sandbox environments
  • Many hospitals have a test instance of their interface engine (Mirth, Rhapsody) available for new instrument onboarding

4. Common rejection reasons. These are the errors we see most often during hospital onboarding:

ErrorCauseFix
AE (Application Error) in ACKMRN not found in target systemVerify PID-3 assigning authority matches
AR (Application Reject)Unknown sending applicationRegister MSH-3 in the interface engine's allowed senders
Message accepted but result not filedOBR-4 CPT code not mappedWork with lab director to map your test code
Duplicate message rejectedNon-unique MSH-10Fix your message control ID generator
Patient mismatchWrong MRN formatSome systems expect leading zeros, some do not

5. ACK message handling. The receiving system responds to every ORU^R01 with an ACK message. You must parse this response:

def parse_ack(ack_message: str) -> tuple[str, str]:
    """
    Parse an HL7v2 ACK message.
    Returns (ack_code, error_message).
    
    ACK codes:
      AA = Application Accept (success)
      AE = Application Error (message received but not processed)
      AR = Application Reject (message rejected)
    """
    segments = ack_message.strip().split("\r")
    msa_segment = None
    for seg in segments:
        if seg.startswith("MSA"):
            msa_segment = seg
            break
 
    if not msa_segment:
        return ("UNKNOWN", "No MSA segment in ACK")
 
    fields = msa_segment.split("|")
    ack_code = fields[1] if len(fields) > 1 else "UNKNOWN"
    error_msg = fields[3] if len(fields) > 3 else ""
 
    return (ack_code, error_msg)

You must implement retry logic for AE responses (transient errors) and alerting for AR responses (configuration problems that require human intervention).

Transport: TCP/IP with MLLP

HL7v2 messages are transmitted over TCP using the Minimal Lower Layer Protocol (MLLP). MLLP wraps each message with a start byte (0x0B), end byte (0x1C), and a trailing carriage return (0x0D). Every HL7v2 interface engine expects MLLP framing.

import socket
 
MLLP_START = b"\x0b"
MLLP_END = b"\x1c\x0d"
 
 
def send_hl7_message(
    message: str,
    host: str,
    port: int,
    timeout: float = 30.0,
) -> str:
    """
    Send an HL7v2 message via MLLP and return the ACK response.
    """
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.settimeout(timeout)
        sock.connect((host, port))
 
        # Wrap message in MLLP envelope
        mllp_message = MLLP_START + message.encode("utf-8") + MLLP_END
        sock.sendall(mllp_message)
 
        # Receive ACK (also MLLP-wrapped)
        response = b""
        while True:
            chunk = sock.recv(4096)
            if not chunk:
                break
            response += chunk
            if MLLP_END[0:1] in response:
                break
 
        # Strip MLLP framing from response
        ack = response.replace(MLLP_START, b"").replace(MLLP_END, b"")
        return ack.decode("utf-8")

In production, you need several additional layers:

  • Persistent connection pool to avoid repeated TCP handshakes
  • TLS encryption (most hospitals require it)
  • Message queue (Redis, RabbitMQ) between your instrument software and the MLLP sender

Never send HL7 messages synchronously from the instrument UI thread - network issues should not block the clinician.

The FHIR alternative: DiagnosticReport

While HL7v2 is the pragmatic choice today, FHIR adoption is accelerating. If your target hospital uses FHIR R4 for lab results - or if you are building for a greenfield deployment - here is the equivalent structure as a FHIR DiagnosticReport resource:

{
  "resourceType": "DiagnosticReport",
  "id": "spectradx-ftir-001",
  "status": "final",
  "category": [
    {
      "coding": [
        {
          "system": "http://terminology.hl7.org/CodeSystem/v2-0074",
          "code": "MB",
          "display": "Microbiology"
        }
      ]
    }
  ],
  "code": {
    "coding": [
      {
        "system": "http://loinc.org",
        "code": "6558-6",
        "display": "Streptococcus pyogenes Ag [Presence] in Throat"
      }
    ]
  },
  "subject": {
    "reference": "Patient/mrn001234"
  },
  "effectiveDateTime": "2026-05-08T14:25:00Z",
  "issued": "2026-05-08T14:30:22Z",
  "performer": [
    {
      "reference": "Organization/spectradx-main-lab"
    }
  ],
  "result": [
    {
      "reference": "Observation/strep-a-result"
    },
    {
      "reference": "Observation/classification-confidence"
    }
  ]
}

The referenced Observation resource for the classification result:

{
  "resourceType": "Observation",
  "id": "strep-a-result",
  "status": "final",
  "code": {
    "coding": [
      {
        "system": "http://loinc.org",
        "code": "6558-6",
        "display": "Streptococcus pyogenes Ag [Presence] in Throat"
      }
    ]
  },
  "valueCodeableConcept": {
    "coding": [
      {
        "system": "http://snomed.info/sct",
        "code": "10828004",
        "display": "Positive"
      }
    ]
  },
  "interpretation": [
    {
      "coding": [
        {
          "system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation",
          "code": "A",
          "display": "Abnormal"
        }
      ]
    }
  ]
}

The FHIR approach is cleaner. It is self-describing, uses standard REST APIs, and the JSON structure is straightforward to generate and validate. But you will still need the HL7v2 path for the majority of hospital integrations in 2026.

Regulatory considerations

If your spectroscopy instrument is a clinical diagnostic device (and if it is generating results that go into patient charts, it is), your HL7 output is part of your regulatory submission. For FDA 510(k) or De Novo submissions:

  • The HL7 message format must be documented in your system design specification
  • Message validation must be part of your verification and validation (V&V) protocol
  • You must demonstrate that the message accurately represents the instrument's output - no data transformation errors between the classifier output and the HL7 message
  • Corrected result handling (OBR-25 = C) must have a documented audit trail

For IVD software under the EU IVDR, the HL7 interface is part of your interoperability documentation and must be validated against the target LIS during clinical evaluation.

What we built

HL7 integration is one of those things that looks simple on paper and consumes months in practice. The message format itself is straightforward. The complexity is in the details: MRN format variations between hospitals, CPT code mapping, vocabulary management, transport reliability, correction workflows, and the sheer number of edge cases that only surface during live hospital onboarding.

SpectraDx handles HL7v2 ORU^R01 generation, MLLP transport, ACK processing, and FHIR DiagnosticReport output as built-in platform capabilities. Every spectral classification result flows through a validated message pipeline that has been tested against Epic, Oracle Health (Cerner), and MEDITECH interfaces.

If you are building spectroscopy-based diagnostics and want to skip the months of HL7 integration work, get in touch. If you are interested in how this fits into the broader clinical workflow, read Building Clinical Workflow Software for Spectroscopy-Based Diagnostics.

Frequently Asked Questions

Generate an HL7v2 ORU^R01 message with your classification result mapped to OBX segments, then transmit it over MLLP (Minimum Lower Layer Protocol) to the hospital's integration engine. The integration engine routes the message to the EHR. You will need the hospital's specific HL7 configuration — MRN format, receiving facility codes, and LOINC/CPT mappings — which vary by site.

ORU^R01 (Observation Result Unsolicited) is the standard HL7v2 message type for sending lab results from an instrument or LIS to an EHR. It contains patient demographics in the PID segment, order information in OBR, and the actual results in one or more OBX segments. Nearly every hospital EHR accepts this message type.

Yes. Epic accepts HL7v2 ORU^R01 messages via MLLP, typically routed through an integration engine like Rhapsody or Mirth Connect. The spectroscopy system generates the ORU message with the classification result, confidence score, and appropriate LOINC codes, then sends it to the integration engine which forwards it to Epic's inbound results interface.

HL7v2 uses pipe-delimited text messages sent over MLLP — it is the dominant standard in hospitals today and what most EHR interfaces expect. FHIR uses RESTful JSON APIs and is cleaner to implement but less widely deployed for lab result delivery. Most spectroscopy systems need HL7v2 support for current hospital integrations, with FHIR as an option for modern EHR deployments.

SpectraDx builds clinical workflow software for spectroscopy-based diagnostics.

The layer between the spectrometer and the clinician. Instrument control, patient workflow, ML classification, HL7/FHIR output, and billing — in one platform.

Get articles like this in your inbox.

Monthly technical resources for spectroscopy professionals. No marketing fluff.