Taction Software — FHIR Integration with Mirth Connect
Blog·April 30, 2026·Taction Software

How to Build an HL7 Interface in Mirth Connect: A Complete 2026 Tutorial

A practitioner-level walkthrough for building a working HL7 v2 interface in Mirth Connect — from channel design to production hardening, with code examples for filters, transformers, ACK handling, and dead-letter routing.

Mirth ConnectHL7TutorialIntegration
TL;DR

Build a production-grade HL7 interface in Mirth Connect by working through ten steps: design the spec, create the channel, configure an MLLP listener, filter by message type, write a defensive source transformer that drops a structured payload into channelMap, configure the destination with authentication and queueing, write the destination transformer, configure ACK timing so the sender is acknowledged only after durable persistence, throw on errors and route exhausted retries to a dead-letter channel, then test statically, with sample messages, end-to-end, and under load before hardening security, reliability, observability, and operations for go-live.

What you'll build

A complete production-grade HL7 v2 interface in Mirth Connect that receives ADT^A01 admission messages over MLLP, transforms them, and writes them to a downstream system over HTTPS. By the end of this tutorial you'll have a working channel, a tested transformer, proper ACK handling, error recovery, and the production hardening needed to run this safely in a real hospital environment. The example uses ADT, but every pattern applies to any HL7 message type — ORM, ORU, SIU, MDM, DFT, and the others.

This is the tutorial we wish we had when we started building HL7 interfaces in Mirth Connect. Most "build your first interface" content stops at "configure a listener" — which gets you to a demo but nowhere close to a production-ready interface. The hard parts are the parts everyone skips: ACK handling, error recovery, character encoding, performance under load, and the production hardening that prevents 2 AM pages.

This guide walks through every layer. It assumes basic familiarity with HL7 v2 (if you need a primer, start with our HL7 integration complete guide) and an installed Mirth Connect (see our Mirth Connect installation guide if you don't have one yet). Pair it with our Mirth Connect complete guide for product-level context. Backed by the engineers who deliver Mirth Connect support for US healthcare organizations.

1. Prerequisites

Before starting:

  • Mirth Connect installed and the Administrator running. Latest stable version recommended; this tutorial works on 4.x.
  • Basic HL7 v2 knowledge — segments, fields, components. If terms like MSH, PID, ADT^A01, and ACK are unfamiliar, read our HL7 integration complete guide first.
  • A test HL7 sender — Mirth's own File Reader on a sample file works for testing, or use HAPI HL7 v2 testing tools to send live MLLP messages.
  • Access to a downstream system for the destination side, or a test endpoint you can write to. For this tutorial we use an HTTPS POST destination — easy to substitute with a real EHR or analytics platform.

2. Designing the interface before you build it

Before you click anywhere in the Administrator, answer these six questions in writing. Skipping this step is the most common mistake in HL7 interface work.

  1. What message type(s) will the interface accept? ADT, ORM, ORU, SIU, etc. Mixing types in one channel is occasionally appropriate but usually a mistake.
  2. Who is the sender? EHR vendor, version, contact person who can troubleshoot from their side.
  3. Who is the receiver? Downstream system, expected format, contact person, expected response time.
  4. What's the volume? Messages per second under normal load and peak load. This drives source and destination thread counts.
  5. What ACK pattern is required? Application accept / accept-and-process / synchronous vs asynchronous.
  6. What error handling is expected? Reject vs queue-and-retry vs dead-letter.

Document these as a one-page interface specification. We use this exact template across hundreds of client interfaces — every interface that has a written spec ahead of build time ships smoothly; every one that doesn't has unpleasant surprises in production.

For our tutorial:

  • Message type: ADT^A01 (admission notification)
  • Sender: Hospital EHR over MLLP
  • Receiver: Downstream analytics platform via HTTPS POST
  • Volume: ~50 msg/sec peak
  • ACK pattern: Application Accept (AA) on successful persistence
  • Error handling: Retry on destination failure with exponential backoff; dead-letter after 5 failed attempts

3. Step 1 — Create a new channel

In the Mirth Administrator:

  1. Click Channels in the left navigation.
  2. Click New Channel.
  3. In the Summary tab, set:
    • Channel Name: ADT_INBOUND_FROM_EHR_TO_ANALYTICS
    • Description: "Receives ADT^A01 from main EHR over MLLP, transforms to JSON, posts to downstream analytics platform."
    • Initial State: Started
  4. Save.

Naming convention matters at scale. We recommend {MESSAGETYPE}_{DIRECTION}_FROM_{SOURCE}_TO_{DESTINATION} — it scales to 50+ channels without the team getting lost.

4. Step 2 — Configure the source connector (MLLP listener)

The source receives the inbound HL7 message.

  1. In the channel editor, click the Source tab.
  2. Set Connector Type to TCP Listener (Mirth's MLLP listener).
  3. Configure Listener Settings: Listener Type TCP / MLLP, Receive Timeout 30000 ms, Listener Port 6661, Bind Address 0.0.0.0 (or specific interface for production), Reconnect Interval 5000 ms.
  4. Configure MLLP Frame: Start of Block 0x0B, End of Block 1 0x1C, End of Block 2 0x0D — these are defaults; do not change.
  5. Configure Process Batch: false for a single-message interface; set to true if you expect bundled messages.
  6. Source Connector Settings → Connector Threads: start with 4 for moderate volume. See Mirth Connect performance tuning for sizing guidance.

For production with TLS (highly recommended for any cross-trust-boundary MLLP): enable Use SSL/TLS, configure keystore path and credentials, and set minimum TLS version to TLS 1.2 or higher. For deeper context including framing and troubleshooting, see MLLP protocol explained, Mirth Connect SSL/TLS hardening, and Mirth Connect MLLP connection refused.

Leave the rest at defaults for now. Save the channel.

5. Step 3 — Configure the source filter

The filter decides whether a message proceeds to the transformer or is dropped. For our tutorial: only process ADT^A01 messages, ignore everything else.

  1. In the Source tab, click Edit Filter.
  2. Click Add New Rule → choose JavaScript.
  3. Replace the rule body with:
// Only process ADT^A01 messages
var messageType = msg['MSH']['MSH.9']['MSH.9.1'].toString();
var triggerEvent = msg['MSH']['MSH.9']['MSH.9.2'].toString();

if (messageType === 'ADT' && triggerEvent === 'A01') {
    return true;  // Process this message
} else {
    return false; // Filter out (will be logged but not transformed)
}

This filter is fast and cheap — every message hits it. Filters are the right place to drop messages you don't care about; doing the same check inside a transformer wastes CPU. For broader transformer language considerations, see Mirth Connect Groovy vs JavaScript transformers.

6. Step 4 — Build the source transformer

The source transformer extracts data from the HL7 message and prepares it for the destination(s). For our tutorial, we extract patient demographics and visit details into a structured object that the destination can convert to JSON.

  1. In the Source tab, click Edit Transformer.
  2. Click Add New StepJavaScript.
  3. Replace with:
// Extract core ADT^A01 fields
try {
    // Patient demographics from PID
    var patientId = msg['PID']['PID.3']['PID.3.1'].toString();
    var familyName = msg['PID']['PID.5']['PID.5.1'].toString();
    var givenName = msg['PID']['PID.5']['PID.5.2'].toString();
    var birthDate = msg['PID']['PID.7']['PID.7.1'].toString();
    var gender = msg['PID']['PID.8'].toString();

    // Visit info from PV1
    var patientClass = msg['PV1']['PV1.2'].toString();
    var assignedLocation = msg['PV1']['PV1.3']['PV1.3.1'].toString();
    var attendingMd = msg['PV1']['PV1.7']['PV1.7.2'].toString() + ' ' +
                      msg['PV1']['PV1.7']['PV1.7.3'].toString();

    // Event time from EVN or MSH
    var eventDateTime = msg['EVN']['EVN.2']['EVN.2.1'].toString();
    if (!eventDateTime) {
        eventDateTime = msg['MSH']['MSH.7']['MSH.7.1'].toString();
    }

    // Build the structured object — held in channelMap for the destination to use
    var patientEvent = {
        eventType: 'ADT_A01_ADMISSION',
        eventTime: eventDateTime,
        patient: {
            mrn: patientId,
            familyName: familyName,
            givenName: givenName,
            birthDate: birthDate,
            gender: gender
        },
        visit: {
            patientClass: patientClass,
            location: assignedLocation,
            attendingProvider: attendingMd
        },
        sourceSystem: msg['MSH']['MSH.4']['MSH.4.1'].toString(),
        messageControlId: msg['MSH']['MSH.10'].toString()
    };

    channelMap.put('patientEvent', patientEvent);
    channelMap.put('mrn', patientId);  // for logging convenience

} catch (e) {
    // Don't silently swallow — log and rethrow so the channel marks the message as errored
    logger.error('ADT_A01 source transformer failed: ' + e.message);
    throw e;
}

A few production-grade patterns embedded above:

  • Defensive field access — using .toString() and falling back to MSH.7 if EVN.2 is missing.
  • Structured payload in channelMap — keeps the destination decoupled from HL7 parsing. The destination doesn't need to know about HL7; it just sees a clean object.
  • Try/catch with logger.error and rethrow — failures are visible in Mirth's Events view and the channel knows the message failed (rather than silently ACKing bad data).
  • No globalMap useglobalMap persists forever and is a memory leak waiting to happen, as covered in Mirth Connect Java heap space error.

7. Step 5 — Configure the destination

Now the destination side — taking the parsed event and posting it to the analytics platform.

  1. In the channel editor, click the Destinations tab.
  2. Click Add Destination.
  3. Set Destination Name to Analytics Platform HTTPS POST and Connector Type to HTTP Sender.
  4. Configure the connector: HTTP URL https://analytics.example.com/api/v1/patient-events, Method POST, Multipart false, Use Authentication (Bearer Token or API Key per your downstream system), Content Type application/json, Charset UTF-8, Response Timeout 30000 ms — never leave HTTP destinations with infinite timeouts.
  5. Configure Response Handling: response codes 200, 201, 202 should be treated as success.
  6. Configure Queueing: Queue Messages On Failure, Retry Count 5, Retry Interval 60000 ms (multiplied with exponential backoff if you configure that pattern).
  7. Save.

8. Step 6 — Build the destination transformer

The destination transformer prepares the actual HTTP payload from the parsed event in channelMap.

  1. Click Edit Transformer on the destination.
  2. Add a JavaScript step:
// Pull the parsed event from channelMap and JSON-serialize it for HTTP POST
try {
    var event = channelMap.get('patientEvent');
    if (!event) {
        throw new Error('No patientEvent found in channelMap — source transformer must have failed.');
    }

    // For HTTP destinations, set the body via the connectorMessage API:
    var payload = JSON.stringify(event);
    connectorMessage.setEncodedData(payload);

    // Add headers for downstream tracking
    var headers = new Packages.java.util.HashMap();
    headers.put('X-Source-System', 'mirth-adt-inbound');
    headers.put('X-Message-Control-Id', event.messageControlId);
    channelMap.put('outboundHeaders', headers);

} catch (e) {
    logger.error('Destination transformer failed: ' + e.message);
    throw e;
}

The exact HTTP destination API for setting body and headers can vary across Mirth versions — consult your version's documentation. The pattern above demonstrates the principle: pull from channelMap, prepare the outbound payload, throw on failure to fail the channel cleanly.

9. Step 7 — Handle ACKs correctly

This is where most tutorials skip critical material — and where most production HL7 interfaces have subtle bugs.

In the Source tab, look for Response settings. You're configuring what gets sent back to the EHR after Mirth processes the message.

The wrong way (common bug)

Configuring Mirth to send an ACK immediately when the message arrives means the EHR thinks the message succeeded before Mirth has actually persisted it or sent it downstream. If Mirth crashes between accept and downstream success, the message is lost — and the EHR has no idea.

The right way

Configure Mirth's source response to send an ACK after the message is durably persisted (or processed downstream). In the Source → Response settings, choose Auto-generate (After source transformer) for default cases, or Wait For Destinations if you want to ACK only after destinations complete (requires synchronous destination handling).

Custom ACK content

If you need a customized ACK (specific MSA codes, ERR segments on application errors), build it in a Response transformer. The basic pattern:

// Generate an HL7 ACK (AA = Application Accept, AE = Application Error, AR = Reject)
var msgControlId = sourceMap.get('originalMessageControlId') ||
                   msg['MSH']['MSH.10'].toString();
var ack = 'MSH|^~\\&|MIRTH|TACTION|' +
          msg['MSH']['MSH.3']['MSH.3.1'].toString() + '|' +
          msg['MSH']['MSH.4']['MSH.4.1'].toString() + '|' +
          new Date().toISOString().replace(/[-:.TZ]/g, '').substring(0, 14) +
          '||ACK^A01|' + msgControlId + '|P|2.5\r' +
          'MSA|AA|' + msgControlId + '\r';

responseMap.put('customAck', ResponseFactory.getSentResponse(ack));

Most production interfaces use auto-generated ACKs — only build custom ACK logic when the EHR has specific requirements. We have a deeper treatment of ACK semantics in our companion guide HL7 ACK/NAK explained.

10. Step 8 — Add error handling

Production HL7 interfaces fail in predictable ways. Configure for them ahead of time.

Source-side errors

If the source transformer throws an exception (as it does in our try/catch above), Mirth marks the message as ERROR and stores the exception. The default behavior is to NAK the sender, which causes them to potentially retry. For our tutorial, that's the right behavior. Be careful about catching errors silently — it sends AA to the EHR while the message is actually broken on your side, and you'll discover the gap weeks later when someone notices missing data.

Destination-side errors (network, downstream)

In Destination → Queue Settings: Queue Messages On Failure (already configured), Retry Interval starting at 60 seconds (configurable for exponential backoff via destination scripting), Maximum Retries 5. After max retries, the message will sit in the destination queue indefinitely unless you configure dead-letter handling.

Dead-letter handling

For messages that exhaust all retries:

  1. Create a separate dead-letter channel that consumes failed messages from this channel.
  2. Use Mirth's Postprocessor script on the source channel to detect destination failures and forward to the dead-letter channel:
// In Postprocessor — check if any destination failed
for (var i = 0; i < responseMap.size(); i++) {
    var dest = responseMap.values().toArray()[i];
    if (dest && dest.getStatus() == Status.ERROR) {
        // Forward the original message to dead-letter channel
        VMRouter().routeMessage('DEAD_LETTER_ADT', connectorMessage.getRawData());
        break;
    }
}

The dead-letter channel can write to a database, send an email alert, or push to a queue for human review. This is essential for any production interface. We cover broader error-handling and queue strategies in our Mirth Connect performance tuning guide.

11. Step 9 — Test the channel

Don't deploy to production without testing.

Step 9.1 — Static syntax validation

Click Validate on every transformer and filter. Mirth's editor catches basic syntax errors before deploy. Any error here means the channel won't start — see Mirth Connect channel not starting if it doesn't deploy.

Step 9.2 — Sample message test

Use Mirth's built-in test feature. Paste this test ADT^A01:

MSH|^~\&|EPIC|HOSPITAL|MIRTH|TACTION|20260417103045||ADT^A01|MSG00001|P|2.5
EVN|A01|20260417103000
PID|1||123456^^^HOSPITAL^MR||DOE^JOHN^A||19720512|M|||123 MAIN ST^^AUSTIN^TX^78701||(512)555-0134|||M||ACC001||123-45-6789
PV1|1|I|ICU^101^1^HOSPITAL||||1234^SMITH^JANE^A^MD|||MED||||||||VISIT001

Click Process Message to push it through filter, transformer, and into the destination flow. Inspect the result — channelMap contents, destination status, exceptions.

Step 9.3 — End-to-end test with a real sender

Once the channel deploys cleanly:

  1. Have the EHR (or a test sender) push a single live ADT^A01 over MLLP to your listener port.
  2. Watch the Mirth Administrator Dashboard — confirm message count increments.
  3. Verify ACK was returned to the sender.
  4. Verify the downstream system received the HTTP POST.
  5. Inspect the message in View Messages — confirm all transformations executed correctly.

Step 9.4 — Load test before production

For interfaces with meaningful volume (>10 msg/sec sustained), do a load test before go-live. Tools like HAPI MLLP testing tools or scripted Python clients can blast realistic traffic at your channel. Watch for: channel stays in Started state under sustained load; heap stays below 75% (see Java heap space error if it climbs); no queue backlog accumulating on the destination; ACK round-trip times stay under expected SLA; no errors in Events view.

12. Step 10 — Production hardening

A working channel is not the same as a production-ready channel. Before go-live, verify all of the following:

Security

  • TLS enabled on MLLP listener (required for any cross-trust-boundary connection).
  • Strong cipher suites only — TLS 1.2 minimum.
  • Authentication on the destination — Bearer token, mTLS, or API key (never unauthenticated POST).
  • Channel uses non-root account appropriately.
  • PHI not logged in plaintext beyond what's necessary for audit.

For deeper security guidance, see Mirth Connect SSL/TLS hardening and the broader healthcare interoperability and compliance guide.

Reliability

  • Source response configured correctly — ACK sent only after durable persistence.
  • Destination queueing enabled with retry policy.
  • Dead-letter channel configured for messages that exhaust retries.
  • Channel pruning policy set (see Mirth Connect database configuration).
  • Heap sized appropriately for projected volume.

Observability

  • Channel state alerting — any unplanned stop fires a page.
  • Queue depth alerting — destination queue > threshold triggers warning.
  • Error rate alerting — sustained errors above baseline trigger investigation.
  • Logs shipped to SIEM/observability platform (Splunk, Elastic, Datadog).

Operational

  • Channel exported to Git for version control.
  • Runbook documented — what to do if the channel fails, who to call, how to recover.
  • Test environment exists mirroring production, used for any future change.
  • Change-control process in place for production updates.

The list looks long, but each item is a 5–15 minute task that prevents an hours-long incident later. We catalog more production hardening patterns in our Mirth Connect issues and fixes reference.

13. Common variations on this pattern

The ADT-to-HTTPS pattern in this tutorial generalizes to many real-world interfaces. Common variations:

  • Lab results: ORU → downstream EHR. Same channel pattern, different message type filter (ORU^R01 instead of ADT^A01). Source transformer extracts patient + lab results from OBR and OBX segments. See HL7 ORM + ORU lab workflow.
  • EHR-to-EHR ADT bridge. Source: MLLP listener from one EHR. Destination: MLLP sender to another EHR. Transformer maps differences in vendor-specific Z-segments and field codes between the two systems.
  • HL7 v2 to FHIR R4 façade. Source: MLLP HL7 listener. Destination: HTTP POST to a FHIR server. Transformer converts ADT^A01 to FHIR Patient and Encounter resources. One of the most common patterns we build for healthtech clients — see our Mirth FHIR Server offering and the broader FHIR integration guide.
  • Database-driven outbound. Source: Database Reader polling for new rows. Destination: MLLP sender. Transformer constructs HL7 messages from row data. Common pattern for bidirectional EHR integration where the EHR writes to a shared database.
  • Multi-destination fan-out. One inbound ADT^A01 → multiple destinations: analytics platform, audit log, and a second EHR. Each destination has its own transformer. Mirth handles this naturally.

For each variation, the tutorial pattern still applies: design first, listener, filter, source transformer to structured payload, destination(s), ACK strategy, error handling, test, harden.

14. What to build next

Once you have one working interface, the natural next steps:

  1. Add observability — ship channel metrics and logs to your monitoring stack.
  2. Build a second interface — repeat the pattern with a different message type. The third or fourth interface is dramatically faster than the first because you're now reusing patterns.
  3. Standardize across interfaces — Code Templates for shared transformer logic, Configuration Map for environment-specific values.
  4. Set up CI/CD — export channels to Git, run automated syntax checks on commits, deploy via API rather than the Administrator UI for production.
  5. Expand into FHIR — pair the v2 layer with a FHIR server for modern API access. See our FHIR R4 integration complete guide.
  6. Plan for scale — once you have 10+ channels in production, performance tuning starts to matter. Read Mirth Connect performance tuning before scaling further.

For broader integration architecture decisions — when Mirth is the right choice, when an alternative might be — see our comparison library: Mirth Connect vs Rhapsody, Mirth Connect vs Iguana, Mirth Connect alternatives 2026.

FAQ

Frequently Asked Questions

How long does it take to build an HL7 interface in Mirth Connect?
A simple inbound HL7 v2 interface (single message type, one destination) typically takes 1–2 weeks end to end including testing, hardening, and deployment to production. A bidirectional or multi-destination interface takes 3–6 weeks. Complex EHR integrations with vendor-specific quirks can take 2–3 months. Engineers who have built dozens of interfaces ship the first ones faster than first-timers — much of the duration is learning how Mirth handles edge cases.
Do I need to know Java to build an HL7 interface in Mirth?
No. Mirth's primary scripting languages are JavaScript and Groovy, both of which are easier to learn than Java for transformer work. JavaScript is the most widely-used and has the most community examples.
Can Mirth Connect handle FHIR as well as HL7 v2?
Yes. Mirth supports FHIR R4 through HTTP connectors and JSON processing, and is widely used as a FHIR façade in front of legacy HL7 v2 systems.
What's the difference between a filter and a transformer in Mirth?
A filter decides whether a message proceeds; it returns true or false. A transformer modifies the message and adds data to channel scope; it doesn't decide whether to process. Use filters early in the pipeline to drop messages cheaply; use transformers to do the actual work.
How do I send an HL7 ACK from Mirth?
In most cases, configure Mirth's source response setting to auto-generate an ACK after the source transformer (or after destinations complete, depending on your synchronicity needs). For custom ACKs with specific MSA codes, build a Response transformer.
Why is my Mirth channel not receiving messages?
The most common causes: listener not actually running (channel not deployed), wrong port configuration, firewall blocking traffic from sender, or TLS handshake failure. Walk through the diagnostic ladder in our Mirth Connect MLLP connection refused guide.
What's the best way to test an HL7 interface in Mirth?
Three layers: (1) static syntax validation in the editor, (2) sample-message testing using Mirth's Process Message feature, (3) end-to-end testing with a real sender pushing live MLLP traffic. Load testing should be done before production go-live for any interface with meaningful volume.
How do I handle errors in Mirth HL7 channels?
At the source: throw exceptions explicitly when a message can't be transformed, so Mirth marks it as ERROR and NAKs the sender. At destinations: enable queueing with retry policy and a maximum retry count. After max retries, route to a dead-letter channel for human review. Never silently swallow errors.
Can multiple interfaces share the same MLLP port in Mirth?
No — only one channel per port per interface. If you need to multiplex, terminate at one channel and route internally to others using VMRouter.routeMessage(). Two channels listening on the same port produces a BindException at deploy time.
Where do I get expert help with Mirth HL7 work?
Our Mirth Connect helpdesk provides under-15-minute emergency response, and our services team covers full lifecycle Mirth work — interface design, build, migration, performance tuning, 24/7 managed operations.

Need expert Mirth Connect support?

Whether you have a one-time integration project or need ongoing managed support, every engagement is named, scoped, and priced upfront — productized packages, no hourly billing.

Talk to a Mirth Solutions Architect

60-second form. Senior engineer responds within one business day.

What is 7 + 5 ?