Receiving HL7 v2 takes an MLLP listener, a parser and an acknowledgment responder. This guide compares the Python libraries that provide them and sets up a production listener.
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class ADTListener(Service):
name = 'demo.hl7.adt-listener'
def handle(self) -> 'None':
# The MLLP channel parsed the message,
# the acknowledgment is sent for you
msg = self.request.input
patient_name = msg.pid.patient_name.family_name
control_id = msg.msh.message_control_id
self.logger.info('Received %s: %s',
control_id, patient_name)
Over MLLP - the Minimal Lower Layer Protocol, a thin TCP framing that hospital systems have used for decades. Each message arrives wrapped in a start byte and an end sequence on a TCP port, and the receiver must reply with an HL7 acknowledgment, an ACK message.
A working receiver therefore needs three parts - an MLLP listener that accepts TCP connections and unwraps frames, a parser that turns the pipe-delimited ER7 payload into something a program can read, and an ACK responder that tells the sender the message was accepted or rejected.
Three main options cover different amounts of the job. python-hl7 and hl7apy are parsing libraries with basic MLLP pieces, Zato is an integration platform where MLLP channels are production infrastructure with routing, security and multi-server scaling around them.
| Library | License | Parsing | MLLP | Production features |
|---|---|---|---|---|
| python-hl7 | BSD | Generic, position-based | Experimental asyncio client and server | None - a library, not a server |
| hl7apy | MIT | Validation against message profiles | A built-in MLLPServer class | None - single-threaded reference server |
| Zato | AGPLv3 | Typed classes for every v2.9 segment | Production MLLP channels | ACKs, routing, deduplication, TLS, REST bridge, multi-server scaling |
For a script that parses a file of messages, a parsing library is enough. For a listener that hospital systems connect to around the clock, the production concerns - acknowledgments, retransmissions, routing, monitoring, high availability - are most of the work, and that is what a platform provides.
Create an MLLP channel in the Dashboard - name, TCP port and the service that will handle each message. The channel unwraps the MLLP framing, parses the ER7 payload into a typed message object, invokes the service and sends the acknowledgment back - an AA when the service completes, an AE with error details when it raises an exception.
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class LabResultListener(Service):
name = 'demo.hl7.lab-result-listener'
def handle(self) -> 'None':
# The channel already parsed the raw ER7 bytes ..
msg = self.request.input
# .. read what the integration needs ..
control_id = msg.msh.message_control_id
message_type = msg.get('MSH.9.1')
# .. convert the whole message to JSON for downstream systems ..
as_json = msg.to_json()
self.logger.info('Received %s (%s), %d bytes of JSON',
control_id, message_type, len(as_json))
# .. returning nothing means Zato replies with an AA acknowledgment.Send a test message to localhost:11553 with any MLLP client and the channel returns the acknowledgment as the response.
Messages parse into typed objects - every segment of the v2.9 specification is a Python class and every field has a semantic name, as documented in field access and path expressions.
from zato.hl7v2 import parse_hl7
raw = (
'MSH|^~\\&|SENDER|FACILITY|RECEIVER|FAC|20260315||ADT^A01^ADT_A01|CTL001|P|2.9\r'
'EVN|A01|20260315\r'
'PID|1||12345^^^HOSP^MR||SMITH^JOHN^A||19800115|M\r'
'PV1|1|I|WARD^101^BED1\r'
)
message = parse_hl7(raw, validate=False)
# Semantic names ..
family_name = message.pid.patient_name.family_name # 'SMITH'
patient_class = message.pv1.patient_class # 'I'
# .. or positional path expressions ..
given_name = message.get('PID.5.2') # 'JOHN'
message_type = message.get('MSH.9.1') # 'ADT'
# .. and the whole message as JSON.
as_json = message.to_json(indent=2)HL7 batch envelopes - FHS and BHS headers wrapping many messages - arrive over the same channels, with the service receiving the full batch to unpack, as described in batch processing. File-based exchange works with the built-in scheduler - a job picks up files on an interval and feeds each message through the same services that handle MLLP traffic, so the processing logic exists once.
Yes - senders wait for the acknowledgment and retransmit the message if none arrives. With Zato the ACK is automatic - AA on success, AE on error, AR when no channel matches - and deduplication catches the retransmissions that still happen.
There is no reserved port - 2575 is a common convention, but every deployment picks its own. The port is part of the MLLP channel definition.
Plain MLLP is unencrypted, so production traffic between organizations runs over TLS. Zato outgoing MLLP connections support TLS and mutual TLS with certificate configuration, and inbound traffic is typically terminated by the network layer in front of the listener.
Yes - MLLP channels have a REST bridge, so the same channel accepts messages over HTTP too, with the same parsing and the same service handling both transports.
Get started with Zato and set up your first MLLP listener in minutes.