Zato receives HL7v2 messages over MLLP and handles framing, parsing, routing, acknowledgments, deduplication, encoding, tolerance for non-standard senders, and optional REST bridging - all configured from the web admin dashboard under Channels > HL7 > MLLP.

If you are new to HL7v2 in Zato, start with the HL7v2 tutorial for a guided walkthrough. Each tab below covers one use case - start with Receiving messages for the basics, then explore the others as your integration needs grow.
A clinical system connects over MLLP, Zato receives the message, parses the ER7 payload, and hands a typed HL7Message object to your service.
Open the web admin dashboard, navigate to Channels > HL7 > MLLP, and click Create a new MLLP channel.

Fill in:
adt-inboundTwo toggles control what your service receives:
Parse on input (on by default) - when on, the service receives a parsed HL7Message object with typed segment and field access. When off, the service receives the raw ER7 string.
Validate - when on, the parser validates the message structure against the HL7 schema (required segments, cardinality rules, choice groups). Invalid messages are rejected with an AE acknowledgment.
With parsing on, your service receives the message as self.request.input:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
if 0:
from zato_hl7v2.base import HL7Message
class ADTHandler(Service):
def handle(self) -> 'None':
# .. the MLLP channel already parsed the raw ER7 bytes ..
msg:'HL7Message' = self.request.input
# .. access segments and fields by name ..
patient_name = msg.pid.patient_name.family_name
visit_date = msg.evn.recorded_date_time
control_id = msg.msh.message_control_id
self.logger.info('Received %s: %s on %s', control_id, patient_name, visit_date)

Send a message from Python:
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^^^HOSP^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.

A single MLLP port can serve multiple channels. Each channel's Routing tab defines which messages it handles based on MSH header fields.

| Field | MSH position | Example values |
|---|---|---|
| Sending application | MSH-3 | WELLNESS_APP, LAB_SYS |
| Sending facility | MSH-4 | MAIN_FAC, EAST_CLINIC |
| Receiving application | MSH-5 | SCHEDULING, HIS |
| Receiving facility | MSH-6 | MAIN_FAC |
| Message type | MSH-9.1 | ADT, ORM, ORU |
| Trigger event | MSH-9.2 | A04, O01, R01 |
| Processing ID | MSH-11 | P (production), T (training), D (debugging) |
| Version ID | MSH-12 | 2.5, 2.9 |
Only filled fields are checked - empty fields act as wildcards. Matching is case-insensitive.
Create two channels on the same port, each routing to a different service:
ADT channel:
adt-handlerhl7-api.adt-handlerADT
ORM channel:
orm-handlerhl7-api.orm-handlerORMWhen a message arrives, Zato checks each channel's routing filters against the MSH segment. The first match wins.
Turn on the Default toggle in the Routing tab to create a catch-all channel. It handles any message that does not match another channel's filters. Only one default channel is allowed per MLLP listener.

AR (Application Reject) acknowledgment.Some systems send HL7v2 over HTTP instead of MLLP. The REST bridge lets you receive HL7v2 messages over both MLLP and REST using the same channel configuration.
In the Main tab, turn on Use REST. Three additional fields appear:

/hl7/adtcurl -X POST \
-u hl7user:secret123 \
-H "Content-Type: text/plain" \
-d 'MSH|^~\&|SENDER|FAC|RECEIVER|FAC|20240315||ADT^A04|MSG001|P|2.9
EVN|A04|20240315
PID|1||12345||SMITH^JOHN' \
http://localhost:17010/hl7/adt
The same service handles both MLLP and REST messages. The response body contains the ACK.

hl7.rest.<mllp-channel-name>. It is managed automatically - do not edit or delete it directly.MLLP wraps each HL7 message in a frame: a start byte, the ER7 payload, and an end sequence.
Where <VT> is 0x0B, <FS> is 0x1C, and <CR> is 0x0D.
The Protocol tab lets you adjust wire-level settings:

| Field | Default | Description |
|---|---|---|
| Start sequence | 0b | Hex bytes marking the start of a frame |
| End sequence | 1c 0d | Hex bytes marking the end of a frame |
| Max message size | 2 MB | Messages larger than this are rejected |
| Max message size unit | MB | kB or MB |
| Read buffer size | 32768 | TCP read buffer in bytes |
| Receive timeout | 250 ms | Socket read timeout in milliseconds |
If a sender omits the start byte but the payload begins with MSH, Zato detects this and treats it as a valid frame start. This handles legacy systems that do not fully comply with the MLLP framing specification.
When a message exceeds the configured maximum size, Zato closes the connection. Increase the limit if you expect large messages (e.g. with embedded PDF or image data in OBX segments).
Zato automatically generates HL7 acknowledgments (ACKs) for every received message.
| Code | Meaning | When |
|---|---|---|
| AA | Application Accept | Service processed the message successfully |
| AE | Application Error | Service raised an exception, or validation failed |
| AR | Application Reject | No routing match and no default channel |
Every ACK contains:
ACK message typeAA, AE, or AR) and the original message's control ID from MSH-10The Return errors toggle controls what the sender sees on failure:

If your service returns a string, Zato sends it as the response instead of generating an ACK. If the service returns nothing (None), Zato generates the standard AA acknowledgment.
Clinical systems often retransmit messages when they do not receive a timely ACK. Deduplication prevents the same message from being processed twice.
Zato extracts the Message Control ID from MSH-10 of each incoming message. If the same control ID was already seen within the configured time-to-live window, the message is treated as a duplicate:
Open the Dedup tab:

| Field | Default | Description |
|---|---|---|
| Dedup TTL | 14 | How long to remember control IDs |
| Dedup TTL unit | Days | Minutes, Hours, or Days |
Set the TTL to 0 or leave it empty to disable deduplication entirely.
HL7 messages can arrive in different character encodings depending on the sending system's locale and configuration.

| Field | Default | Description |
|---|---|---|
| Default character encoding | UTF-8 | Encoding used to decode message bytes |
| Use MSH-18 encoding | On | Read the encoding from MSH-18 instead of using the default |
Available encodings: UTF-8, ISO-8859-1, Windows-1252, US-ASCII.
When Use MSH-18 encoding is on, Zato reads the character set identifier from the MSH-18 field of each incoming message and maps it to a Python codec:
| MSH-18 value | Python codec |
|---|---|
ASCII | us-ascii |
8859/1 | iso-8859-1 |
8859/2 | iso-8859-2 |
8859/3 | iso-8859-3 |
8859/4 | iso-8859-4 |
8859/5 | iso-8859-5 |
8859/6 | iso-8859-6 |
8859/7 | iso-8859-7 |
8859/8 | iso-8859-8 |
8859/9 | iso-8859-9 |
UNICODE UTF-8 | utf-8 |
If MSH-18 is empty or contains an unrecognized value, the default character encoding is used.
ACK responses are always encoded with the default character encoding.
Real-world clinical systems do not always produce standard-compliant HL7 messages. The Tolerance tab provides two groups of toggles: wire-level preprocessing (applied to raw bytes before parsing) and parser-level tolerance (content fixups applied by the Rust ER7 parser during parse_hl7).

All toggles are on by default.
| Toggle | What it does |
|---|---|
| Normalize line endings | Converts LF (\n) and CRLF (\r\n) to CR (\r), which is the HL7-standard segment separator. Without this, messages from Windows-based systems would fail to parse. |
| Force standard delimiters | Rewrites MSH-2 to ^~\& and translates all field, component, subcomponent, and repetition delimiters in the message body to the standard characters. Handles senders that use non-standard delimiters. |
| Repair truncated MSH | Pads the MSH segment if it has fewer than 12 fields. Some legacy systems send minimal MSH headers missing optional trailing fields. |
| Split concatenated messages | Splits multiple messages received in a single MLLP frame. Each sub-message starts with MSH. Each is routed and processed independently. |
| Use MSH-18 encoding | Reads the character encoding from MSH-18 (see the Encoding tab for details). |
All toggles are on by default unless noted otherwise.
| Toggle | Default | What it does |
|---|---|---|
| Fill empty OBX-2 value type | on | When OBX-2 is empty but OBX-5 contains data, fills OBX-2 with ST. Common with lab systems that omit the value type for string results. |
| Replace invalid OBX-2 value type | on | Replaces unrecognized OBX-2 data types with ST so the observation value can still be accessed. |
| Strip orphan escape characters | on | Removes stray backslash characters that do not form a valid HL7 escape sequence. |
| Clear OBX-8 literal null | on | Clears OBX-8 (Abnormal Flags) when it contains the literal string null instead of a valid flag. |
| Strip multi-quote sequences | on | Strips sequences of two or more consecutive double-quote characters that some systems emit as empty-field placeholders. |
| Pad short encoding characters | on | Pads MSH-2 with standard encoding characters when the sender provides fewer than the required four. |
| Fix off-by-one field index | off | Removes a spurious empty first field from non-MSH segments. Some legacy systems prepend a leading field separator that shifts all field indices by one. |
Tolerance preprocessing runs in this order:
HL7 supports batch envelopes using BHS (Batch Header) and FHS (File Header) segments. Some systems wrap multiple messages in a single batch transmission.
If the payload of an MLLP frame starts with BHS| or FHS|, Zato treats the entire frame as a batch:
MSH| line in the batchParse on input = off) so the service can unpack individual messages from the batchFHS|^~\&|SENDER|FAC|RECEIVER|FAC|20240315120000
BHS|^~\&|SENDER|FAC|RECEIVER|FAC|20240315120000
MSH|^~\&|SENDER|FAC|RECEIVER|FAC|20240315120000||ADT^A04|MSG001|P|2.9
EVN|A04|20240315120000
PID|1||12345||SMITH^JOHN
MSH|^~\&|SENDER|FAC|RECEIVER|FAC|20240315120001||ADT^A04|MSG002|P|2.9
EVN|A04|20240315120001
PID|1||67890||DOE^JANE
BTS|2
FTS|1
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class BatchHandler(Service):
def handle(self) -> 'None':
# .. the raw batch string, not parsed ..
raw_batch:'str' = self.request.input
# .. split on MSH boundaries ..
messages = raw_batch.split('MSH|')
# .. skip the FHS/BHS preamble (first element) ..
for raw_message in messages[1:]:
full_message = 'MSH|' + raw_message.rstrip()
self.logger.info('Processing message: %s', full_message[:80])
The Logging tab controls how much detail Zato records about MLLP traffic.

| Field | Default | Description |
|---|---|---|
| Logging level | INFO | Minimum severity for log entries (DEBUG, INFO, WARNING, ERROR) |
| Log messages | Off | When on, logs the full request and response payloads |
Always logged (at the configured level):
When Log messages is on:
Instead of configuring channels through the web admin dashboard, you can define them declaratively in a YAML file and import them with enmasse. This is especially useful for automated deployments and version-controlled configuration.
channel_hl7_mllp:
- name: my.hl7.lab-results
service: hl7.process-lab-results
should_validate: true
msh9_message_type: ORU
- name: my.hl7.admissions
service: hl7.process-admissions
msh9_message_type: ADT
msh9_trigger_event: A01
See the enmasse reference for the complete list of fields.
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."