# 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 is recorded 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](https://zato.io/docs/dev/healthcare/hl7v2/mllp/index.html).

## The path of a message {#the-path-of-a-message}

```yaml
name: Sending over MLLP
actors:
  - id: service
    label: Your service
  - id: connection
    label: Outgoing connection
  - id: remote
    label: Remote endpoint
steps:
  - from: service
    to: connection
    label: self.mllp['EHR Main'].send(data)
    align: left
  - from: connection
    to: remote
    label: The framed message
  - from: remote
    to: connection
    kind: return
    label: The acknowledgment
  - from: connection
    to: connection
    label: MSA-2 is checked against the control ID sent
  - from: connection
    to: service
    kind: return
    label: An AckResult with the outcome
    align: left
```

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](https://zato.io/docs/dev/healthcare/audit-log.html).

## Create the connection {#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`:

**Dashboard**

Go to Connections ▹ Outgoing ▹ HL7 ▹ MLLP, click **Create a new connection** and complete the wizard:

1. **Name:** EHR Main
2. **Address:** 10.20.30.40:2575
3. Go through the delivery options to the review page and click **Save**

[Video: New MLLP outgoing connection](https://zatosource-production.b-cdn.net/docs/gfx/outgoing/hl7-mllp-create.webm?v=1790019247)

Dashboard menu: Connections > Outgoing > HL7 > MLLP

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

**Enmasse YAML**

The same connection defined in an [enmasse](https://zato.io/docs/admin/enmasse-reference.html#hl7-mllp-outgoing-connections) file, which is how version-controlled deployments create it:

```yaml
outgoing_mllp:

  - name: EHR Main
    address: 10.20.30.40:2575
```

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](https://zato.io/docs/dev/healthcare/hl7v2/mllp/outgoing.html#connection-fields) below.

## Send from a service {#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:

```python
# -*- 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:

```text
INFO - EHR Main answered AA for MSG00001
```

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 {#read-the-acknowledgment}

The `AckResult` fields - `ack_code`, `is_accepted`, `should_retry`, `error_text` and `ack_text` - are listed on the [acknowledgments page](https://zato.io/docs/dev/healthcare/hl7v2/mllp/acknowledgments.html), 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 {#retries-and-pauses}

A message that no acknowledgment came back for is retried, queued and, if it never gets through, dead-lettered, through the same delivery mechanism as other outgoing connections - the settings are in the connection's **Delivery** tab and [Delivery retries and dead-letter queues](https://zato.io/docs/dev/delivery.html) is the page that explains them. An acknowledgment of any code, including a negative one, is the remote system's answer and reaches your code as it arrived - `should_retry` reflects what the remote system asked for and you decide what to do with it.

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](https://zato.io/docs/dev/healthcare/hl7v2/mllp/outgoing.html#connection-fields) below.

## The invoke screen {#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:

```text
AckResult(ack_code='AA', is_accepted=True, should_retry=False, error_text='')
```

## TLS and mTLS {#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 {#connection-fields}

Every field of an outgoing connection, with its default. The fields of the **Delivery** tab are the same for every outgoing connection and [Delivery retries and dead-letter queues](https://zato.io/docs/dev/delivery.html) covers them.

| 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 size** | 2097152 | The biggest message sent and 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. |
| **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 contain 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](https://zato.io/docs/dev/healthcare/audit-log.html). |

## See also {#see-also}

- [Receiving over MLLP](https://zato.io/docs/dev/healthcare/hl7v2/mllp/index.html) - Accept HL7 v2 messages from clinical systems
- [Acknowledgments](https://zato.io/docs/dev/healthcare/hl7v2/mllp/acknowledgments.html) - Every AckResult field and what each code means
- [The audit log](https://zato.io/docs/dev/healthcare/audit-log.html) - Every sent message with its acknowledgment, including outstanding ones
- [Enmasse reference](https://zato.io/docs/admin/enmasse-reference.html) - The connection fields as YAML keys, for version-controlled deployments

## Learn more {#learn-more}

- [Healthcare interface engine](https://zato.io/docs/dev/healthcare/) - Clinical messages, FHIR, conversion and operations
- [HL7 v2 parsing](https://zato.io/docs/dev/healthcare/hl7/v2/parsing/) - Messages as typed objects, with field access and validation
- [MLLP channels](https://zato.io/docs/dev/healthcare/hl7v2/mllp/) - Receiving and sending HL7 v2 over MLLP sockets
- [FHIR integrations](https://zato.io/docs/dev/healthcare/hl7/fhir/) - Read and write FHIR resources, with paths, bundles and extensions
- [HL7 v2 to FHIR](https://zato.io/docs/dev/healthcare/hl7/to-fhir/) - Converting v2 messages into FHIR bundles, codes and references included
- [EDIFACT](https://zato.io/docs/dev/healthcare/edifact/) - Parsing interchanges, dialects and the transports they arrive on
- [Transformation](https://zato.io/docs/dev/healthcare/transformation/) - One message in, another standard out, in ordinary Python
- [Audit log](https://zato.io/docs/dev/healthcare/audit-log.html) - Every message with its acknowledgment, searchable by patient identifier
- [AI in clinical interfaces](https://zato.io/docs/dev/healthcare/ai/) - Services calling LLMs and AI agents calling clinical services as tools
