Sending HL7 v2 over MLLP
Deliver HL7 v2 messages to clinical systems, with each acknowledgment back as a Python object.
An outgoing MLLP connection is a named pool of connections to one remote endpoint - an EHR, a lab system, anything that listens for HL7 v2. Services send through it with self.mllp and receive the remote system's acknowledgment back as a Python object, while every exchange lands in the audit log.
This page shows how to create the connection, send from a service, read the acknowledgment and turn on TLS. For the receive side - accepting messages from clinical systems - go to the receiving guide.
How a message travels
The connection frames the message, sends it and reads the reply, checking that its MSA-2 echoes the control ID of the message it answers - a reply naming another message is reported as an error rather than trusted. Both the message and its acknowledgment are recorded in the audit log.
Create the connection
The Dashboard steps and the enmasse YAML below create the same connection - named EHR Main, pointing at 10.20.30.40:2575:
Go to Connections ▹ Outgoing ▹ HL7 ▹ MLLP, click Create a new connection and complete the wizard:
- Name: EHR Main
- Address: 10.20.30.40:2575
- Go through the delivery options to the review page and click Save
Is anything listening? checks the address before anything is saved - it opens a connection to the endpoint, with TLS when one is configured, and reports whether something accepted it.
The same connection defined in an enmasse file, which is how version-controlled deployments create it:
A name and an address are all a connection needs - every other field has a default that matches the MLLP standard. The full list is in Connection fields below.
Send from a service
Services reach the connection through self.mllp, by name. The send method accepts ER7 text or bytes and returns an AckResult with the outcome:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
# #####################################################################
# #####################################################################
class SendADT(Service):
""" Sends an admission message to the EHR.
"""
def handle(self) -> 'None':
# The message to deliver ..
data = (
'MSH|^~\\&|ZATO|HOSP|EHR|HOSP|20260315120000||ADT^A01|MSG00001|P|2.9\r'
'EVN|A01|20260315120000\r'
'PID|1||12345^^^HOSP^MR||SMITH^JOHN||19800115|M\r'
'PV1|1|I'
)
# .. send it through the connection created above ..
conn = self.mllp['EHR Main']
result = conn.send(data)
# .. and log what the EHR answered.
if result.is_accepted:
self.logger.info(f'EHR Main answered {result.ack_code} for MSG00001')
else:
self.logger.warning(f'EHR Main refused MSG00001: {result.ack_code} {result.error_text}')
A delivered message produces one log line:
Each call to send takes a connection from the pool for the duration of the exchange and returns it afterwards. A name that does not exist raises KeyError, and when no acknowledgment arrives within the timeout, the connection raises an exception with the reason - the audit log files such an exchange under Outstanding, together with the message that is still waiting for its reply.
Read the acknowledgment
The AckResult fields - ack_code, is_accepted, should_retry, error_text and ack_text - are listed on the acknowledgments page, together with what each code means in both directions. The two that decide what happens next are is_accepted, which reports that the message arrived, and should_retry, which reports that the remote system asked for the same message again later.
Retries and pauses
A failed send is retried five times by default, with the delay before each attempt doubling from one second up to 300, plus a 10% random jitter that keeps many senders from retrying at the same moment. When half of the sends within a rolling 60-second window fail, sending to the endpoint pauses for 60 seconds before a trial message goes through again. Each of these numbers is a connection field, listed with the others in Connection fields below.
A direct send from a service reports its outcome at once and leaves the retry decision to your code - should_retry carries what the remote system asked for.
The invoke screen
Every connection in the Connections ▹ Outgoing ▹ HL7 ▹ MLLP list has an Invoke link for sending test messages, no code required. Paste an ER7 message, adjust the timeout if needed - it starts as the connection's Invoke page timeout - and click Submit. The response area shows the AckResult the remote endpoint's reply produced:
TLS and mTLS
Three fields, each a path to a PEM file, control TLS:
| Field | What it does |
|---|---|
| CA bundle | The bundle the remote server's certificate is verified against - setting it turns TLS on. |
| Client certificate | The certificate presented to the server, for mutual TLS. |
| Client key | The private key matching that certificate. |
With the CA bundle alone, the connection uses TLS and verifies the server. With the certificate and key as well, it presents them back - mutual TLS. The minimum protocol version is TLS 1.2, and with no CA bundle the connection is plaintext.
Connection fields
Every field of an outgoing connection, with its default:
| Field | Default | What it does |
|---|---|---|
| Name | - | Identifies the connection - services send through it with self.mllp[name]. |
| Active | on | Services cannot look up an inactive connection. |
| Address | - | The remote endpoint as host:port, e.g. 10.20.30.40:2575. |
| Start bytes | 0b | The bytes each frame opens with, in hex. |
| End bytes | 1c 0d | The bytes each frame closes with, in hex. |
| Max reply size | 2097152 | The biggest acknowledgment accepted, in bytes - 2 MB. |
| Read buffer | 32768 | The socket read buffer, in bytes. |
| Wait for the acknowledgment | 250 | How long the reply is waited for, in milliseconds. |
| Invoke page timeout | 5 | The default timeout for test messages from the invoke screen, in seconds. |
| Pool size | 10 | How many connections the pool keeps open - each concurrent send uses one. |
| Attempts | 5 | How many times a failed send is retried. |
| First wait | 1 | The delay before the first retry, in seconds, doubling with each attempt. |
| Longest wait | 300 | The upper limit for the delay between retries, in seconds. |
| Jitter | 10 | The random percentage applied to each retry delay. |
| Failures | 50 | The failure percentage within the window that pauses sending. |
| Within | 60 | The rolling window the failure percentage is computed over, in seconds. |
| Try again after | 60 | How long sending stays paused before a trial message goes through, in seconds. |
| CA bundle | - | The bundle the server is verified against - setting it turns TLS on. |
| Client certificate | - | The certificate presented for mutual TLS. |
| Client key | - | The private key matching the client certificate. |
| Log each message to the server log | off | Message bodies carry patient data, so turn it on for troubleshooting and back off after. |
| Log level | INFO | The level this connection logs its own entries at. |
| Record each message in the audit log | on | Each message sent and each acknowledgment received goes to the audit log. |
See also
| Page | What it covers |
|---|---|
| Receiving over MLLP | Accept HL7 v2 messages from clinical systems |
| Acknowledgments | Every AckResult field and what each code means |
| The audit log | Every sent message with its acknowledgment, including outstanding ones |
| Enmasse reference | The connection fields as YAML keys, for version-controlled deployments |
Learn more
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