HL7 is the standard for connecting clinical systems - wellness checks, scheduling, lab orders, and more. Zato receives HL7 over MLLP or REST, routes messages by type, and gives you a typed Python API to read, navigate, and convert them.
Receive and process your first HL7 message in under 5 minutes.

Remember: you can connect your AI copilot to Zato documentation for real-time, accurate answers throughout this tutorial.
Three steps: install Zato, create an MLLP channel, write a service. For all MLLP configuration options (framing, encoding, tolerance, dedup, and more), see the full MLLP reference.
Step 1. If you do not have Zato running yet, install it via Docker - it takes under 5 minutes.
Step 2. Open the web admin dashboard at http://localhost:8183, navigate to Channels > HL7 > MLLP, click Create a new MLLP channel and fill in the form:
demo-hl7hl7-api.message-handler (you will create this in the next step)
Your channel is now listening for MLLP connections.

Step 3. Open the Zato IDE at http://localhost:8183, create a new file called hl7_api.py, paste this code, and click Deploy:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
# ################################################################################################################################
# ################################################################################################################################
if 0:
from zato_hl7v2.base import HL7Message
# ################################################################################################################################
# ################################################################################################################################
class MessageHandler(Service):
""" Processes incoming HL7 messages received through an MLLP channel.
"""
def handle(self) -> 'None':
# The MLLP channel already parsed the raw ER7 bytes for us ..
msg:'HL7Message' = self.request.input
# .. each segment is an attribute: msg.pid, msg.evn, msg.msh, msg.pv1 ..
# .. and each field on a segment is also an attribute with a descriptive name.
name = msg.pid.patient_name.family_name
visit_date = msg.evn.recorded_date_time
self.logger.info('Wellness visit: %s on %s', name, visit_date)

Step 4. Test with a Python MLLP client. Open a terminal and send an ADT^A04 message:
from zato.common.hl7.mllp.client import HL7MLLPClient
raw = (
'MSH|^~\\&|WELLNESS_APP|MAIN_FAC|SCHEDULING|MAIN_FAC|'
'20240315120000||ADT^A04^ADT_A01|MSG00001|P|2.9\r'
'EVN|A04|20240315120000\r'
'PID|1||12345^^^FAC^MR||SMITH^JOHN^A||19800115|M\r'
'PV1|1|O\r'
)
client = HL7MLLPClient('localhost', 11223)
response = client.send(raw)
print(response)
You will see an AA acknowledgment - the message was accepted and your service processed it.

msg.msh.sending_application and msg.msh.message_control_id. Redeploy and send the message again.A single MLLP port can serve multiple channels, each handling a different kind of message. The Routing tab on each channel lets you filter by MSH fields (see all routing fields in the MLLP reference).
For example, to set up separate channels for ADT and ORM messages:
adt-handlerhl7-api.adt-handlerADT
orm-handlerhl7-api.orm-handlerORMdefault-handlerhl7-api.default-handler
When a message arrives, Zato checks each channel's routing filters against the MSH segment. The first match wins. If nothing matches, the default channel handles it.
Some systems send HL7 messages over HTTP instead of MLLP. The REST bridge lets you receive HL7 messages over REST using the same MLLP channel configuration - one channel, two protocols.
Step 1. Open an existing MLLP channel (or create a new one) and find the Use REST toggle in the Main tab. Turn it on.
Step 2. Fill in the REST fields that appear:
/hl7/adtStep 3. Choose the mode:
REST only? off (default) - messages are received over both MLLP and REST. The same service handles both.
REST only? on - messages are received only over REST. The MLLP listener is not started. Use this when the sending system only supports HTTP.
Step 4. Click OK. A backing REST channel is created automatically - you do not need to manage it separately.
Once saved, send an HL7 message via HTTP:
curl -X POST http://localhost:11223/hl7/adt \
-H "Content-Type: application/hl7-v2" \
-u admin.invoke:your-password \
-d 'MSH|^~\&|SENDER|FAC|RECV|FAC|20260508||ADT^A01|MSG001|P|2.5\rPID|||12345||DOE^JOHN'
Your service receives and processes the message the same way it does for MLLP - self.request.input is a typed HL7Message object.
The same parser that MLLP channels use internally is available for standalone use. Any ER7 string works - from a file, a queue, a database column, anywhere.
Parse a string
from zato.hl7v2 import parse_hl7
# Any ER7 string works - from a file, a queue, a database column, anywhere
raw:'str' = (
'MSH|^~\\&|WELLNESS_APP|MAIN_FAC|SCHEDULING|MAIN_FAC|'
'20240315120000||ADT^A04^ADT_A01|MSG00001|P|2.9\r'
'EVN|A04|20240315120000\r'
'PID|1||12345^^^FAC^MR||SMITH^JOHN^A||19800115|M\r'
'PV1|1|O\r'
)
# Returns a typed HL7Message with all segments ready to use
msg = parse_hl7(raw)
Navigate segments and fields
Each segment on the message is a Python attribute (msg.pid, msg.evn, msg.msh). Each field on a segment is also an attribute with a descriptive name (patient_name, recorded_date_time, sending_application). You just chain them together:
# PID segment - patient demographics
name = msg.pid.patient_name.family_name
mrn = msg.pid.patient_identifier_list
# EVN segment - event details
visit_date = msg.evn.recorded_date_time
# MSH segment - message header
sender = msg.msh.sending_application
Here are some commonly used fields:
| Segment | Field | What it contains |
|---|---|---|
msg.pid.patient_name.family_name | PID-5 | Family name |
msg.pid.date_time_of_birth | PID-7 | Date of birth |
msg.pid.administrative_sex | PID-8 | Administrative sex |
msg.pid.birth_place | PID-23 | Birth place |
msg.pid.primary_language | PID-15 | Primary language |
msg.pv1.patient_class | PV1-2 | Visit class (inpatient, outpatient, etc.) |
msg.pv1.assigned_patient_location | PV1-3 | Assigned location |
msg.msh.sending_application | MSH-3 | Sending application |
msg.msh.message_type | MSH-9 | Message type |
msg.msh.message_control_id | MSH-10 | Message control ID |
msg.msh.version_id | MSH-12 | HL7 version |
msg.evn.recorded_date_time | EVN-2 | When the event was recorded |
Path-based access
You can also read and update fields using dot-separated paths:
# Read a field using a dot-separated path
name = msg.get('pid.patient_name')
# Update a field the same way
msg.set('pid.birth_place', 'New York')
msg.pv1.patient_class, and log it.Every message, segment, and field can be converted to JSON, dict, or back to ER7.
Convert to JSON
# The whole message as a JSON string
as_json:'str' = msg.to_json(indent=2)
# Or just one segment
pid_json:'str' = msg.pid.to_json(indent=2)
Convert to dict
Serialize back to ER7
# Back to the pipe-delimited ER7 format
er7:'str' = msg.to_er7()
# to_hl7 is an alias for to_er7
er7:'str' = msg.to_hl7()
Batch files
If you receive files containing multiple messages wrapped in FHS/BHS headers, use parse_batch or parse_file:
from zato_hl7v2 import parse_batch, parse_file
# A batch wrapped in BHS/BTS
batch = parse_batch(raw_batch_string)
# A file wrapped in FHS/FTS containing one or more batches
file = parse_file(raw_file_string)
# Iterate over all messages
for msg in file.messages:
self.logger.info('Control ID: %s', msg.msh.message_control_id)
Everything you configured through the dashboard can also be defined declaratively in YAML and deployed with enmasse. This is the recommended approach for production, CI/CD pipelines, and version-controlled infrastructure.
The MLLP channel from this tutorial can be expressed as:
channel_hl7_mllp:
- name: my.hl7.lab-results
service: hl7.process-lab-results
should_validate: true
msh9_message_type: ORU
Save that to a file (e.g. enmasse.yaml) and import it:
See the enmasse reference for all available fields.
msg.pid.patient_name.family_name, msg.msh.sending_applicationFor more details, see the Receiving HL7 over MLLP reference which covers every configuration option: routing, acknowledgments, deduplication, encoding, tolerance, batch messages, and logging.
Landing page with code examples, features and full reference
Routing, acknowledgments, deduplication and REST bridge
Encoding, tolerance, batch mode and all channel settings
Parse ER7-encoded messages into typed Python objects
Access segment fields by name using typed Python objects
Navigate nested fields and components with dot-path syntax
Validate messages against schema definitions
Convert messages between ER7 and JSON formats
All HL7 message structures, grouped by domain
All HL7 segments, grouped by domain
Book a demo with an expert who will help you build meaningful systems that match your ambitions
"For me, Zato Source is the only technology partner to help with operational improvements."