HL7 MLLP tutorial - Route clinical messages
Route HL7 MLLP messages between clinical systems.
With Zato, you can route HL7 MLLP messages between clinical systems in one of two ways, and this tutorial will show you both.

When to use no-code routing
- You pick the receiving destinations in the Dashboard - no code required
- You can assign matching criteria in the Dashboard, e.g. routing only messages whose MSH-3 field is such and such
- All destinations receive the same message in a fan-out fashion, in their own format, e.g. FHIR destinations receive FHIR bundles
- If needed, Python code can be still used to work with the messages before they are delivered
Setting up Zato
- Install Zato through Docker and open the Dashboard at http://localhost:8183
- Confirm the HL7 demo config is imported under System ▹ Demo config Show me where
Accept and route incoming messages
In Zato, incoming messages are accepted through channels, e.g. MLLP channels, REST channels, and others.
So, with the Dashboard open, go to Connections ▹ Channels ▹ HL7 ▹ MLLP, click Create a new MLLP channel and complete the wizard:
- Name: Main HL7 Feed
- Default: on - a default channel receives every message that no other channel matches
- On the second step, open Incoming messages go to and pick all three demo connections
- Go to the review page and click Save
Clicking "Save" created a new MLLP channel which waits for incoming messages now on localhost:11553, and note that we're skipping many customization options you can set, they're documented in their own chapters.
Send a test message
Send this ADT message to localhost:11553 with any MLLP client:
MSH|^~\&|WARD_APP|EAST_WING|EHR|MAIN_HOSPITAL|20260315120000||ADT^A01^ADT_A01|MSG00001|P|2.9
EVN|A01|20260315120000
PID|1||12345^^^FAC^MR||SMITH^JOHN^A||19800115|M
PV1|1|I
What happens next is that the MLLP client receives AA back - indicating the channel accepted the message for delivery, and then the delivery to the destinations takes place.
In other words, the channel returns the acknowledgment first and delivers the message afterwards, so the acknowledgment does not depend on how long delivery takes.
But how is such an incoming message transformed? In what format is it delivered to the destinations?
The answer depends on what kind of destination it is. If it's MLLP, it will be delivered as is. For FHIR, it will be automatically converted into a FHIR bundle. For REST, it will be sent as the body of an HTTP request, with the message exactly as it arrived.
You can click below to see both - the FHIR bundle the test message converts into and the HTTP request the REST archive receives:
The FHIR bundleJSON
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"request": {
"method": "POST",
"url": "MessageHeader"
},
"fullUrl": "urn:uuid:07c9c1d7-d6e4-534a-9ded-772f995bc215",
"resource": {
"resourceType": "MessageHeader",
"eventCoding": {
"system": "http://terminology.hl7.org/CodeSystem/v2-0003",
"code": "A01"
},
"source": {
"name": "WARD_APP",
"endpoint": "urn:zato:hl7v2:authority:WARD_APP"
},
"destination": [
{
"name": "EHR",
"endpoint": "urn:zato:hl7v2:authority:EHR"
}
],
"focus": [
{
"reference": "urn:uuid:26e8a78d-2605-5c33-afcf-5b4409d14be5"
}
]
}
},
{
"request": {
"method": "POST",
"url": "Patient"
},
"fullUrl": "urn:uuid:26e8a78d-2605-5c33-afcf-5b4409d14be5",
"resource": {
"resourceType": "Patient",
"identifier": [
{
"value": "12345",
"system": "urn:zato:hl7v2:authority:FAC",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MR"
}
]
}
}
],
"name": [
{
"family": "SMITH",
"given": [
"JOHN",
"A"
]
}
],
"birthDate": "1980-01-15",
"gender": "male"
}
},
{
"request": {
"method": "POST",
"url": "Encounter"
},
"fullUrl": "urn:uuid:d52ca6a2-5f4c-51d3-871e-8f2f5777074c",
"resource": {
"resourceType": "Encounter",
"subject": {
"reference": "urn:uuid:26e8a78d-2605-5c33-afcf-5b4409d14be5"
},
"class": {
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
"code": "IMP"
},
"status": "in-progress"
}
}
],
"identifier": {
"value": "MSG00001"
},
"timestamp": "2026-03-15T12:00:00+00:00"
}
The REST requestHTTP
Audit log and monitoring
Each channel has an audit log populated with all the messages it receives, along with information what happened next.
This lets you see what actually took place, when, and why, giving you complete observability into the environment, and letting you answer questions about your interfaces.

The audit log is automatically connected to alerts - so if your feeds go silent,

That's it, job well done
And that's it really, that's all there is to it. You create a channel, select your destinations, and the platform does the rest. Naturally, there are many ways to customize the process, documented below, but in a nutshell, it's as easy as that.
When to use Python code routing
- Use it when the decision depends on what is in the message - the channel invokes your service with each message and your code decides where it goes
- You can still assign routing criteria and other options in the Dashboard
- Python code can transform the messages into any format you need, with complete control over where each one is delivered
Setting up Zato
- Install Zato through Docker and log into the Dashboard at http://localhost:8183
- Make sure the HL7 demo config is imported under System ▹ Demo config Show me where
Write the routing service
In Zato, incoming messages are handled by services - Python classes that a channel invokes with each message it accepts.
So, with the Dashboard open, go to the IDE, create a file called hl7_api.py, paste the code below and click Deploy:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
# #####################################################################
# #####################################################################
if 0:
from zato.hl7v2.base import HL7Message
# #####################################################################
# #####################################################################
class MessageHandler(Service):
""" Routes each incoming HL7 message by its type.
"""
def handle(self) -> 'None':
# The decision needs one field - the message code from MSH-9 ..
msg:'HL7Message' = self.request.input
message_type = msg.msh.message_type.message_code
# .. ORU results are forwarded to the EHR ..
if message_type == 'ORU':
connection_name = 'demo.hl7.forward.ehr'
result = self.mllp[connection_name].send(msg)
self.logger.info(f'EHR replied {result.ack_code}')
# .. and everything else stays where it is.
else:
self.logger.info(f'Not forwarding {message_type}')
The moment it deploys, the service is available under the name hl7-api.message-handler - derived from the file and class names - and what it needs now is a channel to invoke it.
Create the channel
Go to Connections ▹ Channels ▹ HL7 ▹ MLLP, click Create a new MLLP channel and complete the wizard, this time picking a service instead of destinations:
- Name: HL7 Message Router
- On the second step, open Service invoked and pick
hl7-api.message-handler - Go to the review page and click Save
Clicking "Save" created a channel that waits for incoming messages on localhost:11553 and invokes your service with each one. A channel with no match rules matches every message, so the router receives everything that arrives - even if the no-code channel from the other tab exists too, because a default channel only gets what nothing else matched.
Send a test message
Send this ADT admission to localhost:11553 with any MLLP client:
MSH|^~\&|WARD_APP|EAST_WING|EHR|MAIN_HOSPITAL|20260315120000||ADT^A01^ADT_A01|MSG00001|P|2.9
EVN|A01|20260315120000
PID|1||12345^^^FAC^MR||SMITH^JOHN^A||19800115|M
PV1|1|I
The client receives AA back and the server log confirms your code made its decision:
But an admission is what this service leaves alone, so how about a message it does forward? Send a lab result next:
MSH|^~\&|LAB_APP|EAST_WING|EHR|MAIN_HOSPITAL|20260315124500||ORU^R01^ORU_R01|MSG00002|P|2.9
PID|1||12345^^^FAC^MR||SMITH^JOHN^A||19800115|M
OBR|1|845439^LAB||1554-5^GLUCOSE^LN|||20260315123000
OBX|1|NM|1554-5^GLUCOSE^LN||182|mg/dL|70-105|H|||F
This time your code sends the message on to demo.hl7.forward.ehr and logs the acknowledgment the receiving system replied with:
And that's all it takes
You deploy a service, point a channel at it, and where each message goes is a decision your Python code makes. The audit log, alerts and everything else from no-code routing work the same way here, and the chapters below cover what more channels can do.
What you built
- A channel that delivers an incoming HL7 feed to destinations picked in the Dashboard, without code
- A routing service that reads one MSH field and decides where each message goes
- A verified path for each - the client's acknowledgment, the audit log's flow view and the server log
What next
- Connect your AI to ask more questions about Zato and to build your interfaces
- Play around with the demo config, point it to your own destinations, add more input matching conditions
- Work in Python on more advanced transformations and routing conditions
- At any time, you can go to System ▹ Demo config, slide the slider off to remove the demo config, and slide it back on to import it again, so don't worry about breaking anything
What else channels can do
| Feature | What it does |
|---|---|
| Routing to selected destinations only | A service picks which destinations receive a given message |
| Match rules | Select a channel by message type, sending application or facility |
| REST bridge | Receive the same messages over HTTP |
| Typed parsing and navigation | msg.pid.patient_name.family_name instead of positional offsets |
| Batch files | Files wrapped in FHS and BHS headers |
| Acknowledgments | What AA, AE and AR mean and where error details appear |
| Deduplication, tolerance and encoding | Handle duplicated messages and senders that deviate from the standard |
| Enmasse YAML | The same channels defined declaratively, in version control |
| The audit log | Every message with its acknowledgment, searchable by patient identifier |
Schedule a meaningful demo
Book a demo with an expert who will help you build meaningful systems that match your ambitions
"We evaluated 12 integration platforms and Zato was the only one to score 100%."
Philip Zuñiga, Assistant Professor, University of the Philippines