Schedule a demo

HL7 Tutorial - Receive, parse, and route clinical messages

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.

In this tutorial

  1. 01Receive HL7 messages over MLLP
  2. 02Route by message type
  3. 03Receive HL7 messages over REST
  4. 04Parse and navigate messages
  5. 05Convert between formats

Remember: you can connect your AI copilot to Zato documentation for real-time, accurate answers throughout this tutorial.

Receive HL7 messages over MLLP

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:

  • Name: demo-hl7
  • Service: hl7-api.message-handler (you will create this in the next step)
  • Leave the other tabs at their defaults for now
  • Click OK

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.

Try it: Modify the service to also log msg.msh.sending_application and msg.msh.message_control_id. Redeploy and send the message again.

Route by message type

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:

  • Name: adt-handler
  • Service: hl7-api.adt-handler
  • In the Routing tab, set Message type to ADT

  • Name: orm-handler
  • Service: hl7-api.orm-handler
  • In the Routing tab, set Message type to ORM
  • Name: default-handler
  • Service: hl7-api.default-handler
  • In the Routing tab, turn on the Default toggle

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.

Note: Routing filters are case-insensitive. You can also filter by trigger event, sending application, sending facility, receiving application, receiving facility, processing ID, and version ID.

Receive HL7 messages over REST

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:

  • URL path - the HTTP path where messages will be received, e.g. /hl7/adt
  • Security - choose a security definition for the REST endpoint
  • Security groups - optionally assign security groups

Step 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.

Note: When you delete an MLLP channel that has REST enabled, the backing REST channel is deleted automatically. If you turn off the Use REST toggle and save, the backing REST channel is also removed.

Parse and navigate messages

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:

SegmentFieldWhat it contains
msg.pid.patient_name.family_namePID-5Family name
msg.pid.date_time_of_birthPID-7Date of birth
msg.pid.administrative_sexPID-8Administrative sex
msg.pid.birth_placePID-23Birth place
msg.pid.primary_languagePID-15Primary language
msg.pv1.patient_classPV1-2Visit class (inpatient, outpatient, etc.)
msg.pv1.assigned_patient_locationPV1-3Assigned location
msg.msh.sending_applicationMSH-3Sending application
msg.msh.message_typeMSH-9Message type
msg.msh.message_control_idMSH-10Message control ID
msg.msh.version_idMSH-12HL7 version
msg.evn.recorded_date_timeEVN-2When 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')
Try it: Parse the sample message above, read the patient class from PV1 using msg.pv1.patient_class, and log it.

Convert between formats

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

# Python dict, ready for further processing
as_dict:'dict' = msg.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)

Deploying with enmasse

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:

zato enmasse /path/to/server --import --input enmasse.yaml

See the enmasse reference for all available fields.

What you built

  • An MLLP channel that receives HL7 messages, parses them automatically, and delivers them to your service as typed Python objects
  • Routing rules that direct different message types to different services
  • A REST bridge that lets the same channel accept HL7 over HTTP, with optional REST-only mode
  • Typed field access - msg.pid.patient_name.family_name, msg.msh.sending_application
  • Format conversion - to JSON, dict, and back to ER7
  • Enmasse deployment - declarative YAML for automated, repeatable channel provisioning

For more details, see the Receiving HL7 over MLLP reference which covers every configuration option: routing, acknowledgments, deduplication, encoding, tolerance, batch messages, and logging.


Schedule a meaningful demo

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."

- John Adams
Program Manager of Channel Enablement at Keysight