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, andACKare 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.
- 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.
- Who is the sender? EHR vendor, version, contact person who can troubleshoot from their side.
- Who is the receiver? Downstream system, expected format, contact person, expected response time.
- What's the volume? Messages per second under normal load and peak load. This drives source and destination thread counts.
- What ACK pattern is required? Application accept / accept-and-process / synchronous vs asynchronous.
- 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:
- Click Channels in the left navigation.
- Click New Channel.
- 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
- Channel Name:
- 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.
- In the channel editor, click the Source tab.
- Set Connector Type to
TCP Listener(Mirth's MLLP listener). - Configure Listener Settings: Listener Type
TCP / MLLP, Receive Timeout30000ms, Listener Port6661, Bind Address0.0.0.0(or specific interface for production), Reconnect Interval5000ms. - Configure MLLP Frame: Start of Block
0x0B, End of Block 10x1C, End of Block 20x0D— these are defaults; do not change. - Configure Process Batch:
falsefor a single-message interface; set to true if you expect bundled messages. - Source Connector Settings → Connector Threads: start with
4for 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.
- In the Source tab, click Edit Filter.
- Click Add New Rule → choose JavaScript.
- 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.
- In the Source tab, click Edit Transformer.
- Click Add New Step → JavaScript.
- 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 toMSH.7ifEVN.2is 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
globalMapuse —globalMappersists 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.
- In the channel editor, click the Destinations tab.
- Click Add Destination.
- Set Destination Name to
Analytics Platform HTTPS POSTand Connector Type toHTTP Sender. - Configure the connector: HTTP URL
https://analytics.example.com/api/v1/patient-events, MethodPOST, Multipartfalse, Use Authentication (Bearer Token or API Key per your downstream system), Content Typeapplication/json, CharsetUTF-8, Response Timeout30000ms — never leave HTTP destinations with infinite timeouts. - Configure Response Handling: response codes
200,201,202should be treated as success. - Configure Queueing: Queue Messages
On Failure, Retry Count5, Retry Interval60000ms (multiplied with exponential backoff if you configure that pattern). - Save.
8. Step 6 — Build the destination transformer
The destination transformer prepares the actual HTTP payload from the parsed event in channelMap.
- Click Edit Transformer on the destination.
- 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:
- Create a separate dead-letter channel that consumes failed messages from this channel.
- 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||||||||VISIT001Click 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:
- Have the EHR (or a test sender) push a single live
ADT^A01over MLLP to your listener port. - Watch the Mirth Administrator Dashboard — confirm message count increments.
- Verify ACK was returned to the sender.
- Verify the downstream system received the HTTP POST.
- 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^R01instead ofADT^A01). Source transformer extracts patient + lab results fromOBRandOBXsegments. 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^A01to FHIRPatientandEncounterresources. 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:
- Add observability — ship channel metrics and logs to your monitoring stack.
- 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.
- Standardize across interfaces — Code Templates for shared transformer logic, Configuration Map for environment-specific values.
- Set up CI/CD — export channels to Git, run automated syntax checks on commits, deploy via API rather than the Administrator UI for production.
- Expand into FHIR — pair the v2 layer with a FHIR server for modern API access. See our FHIR R4 integration complete guide.
- 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.
Related Reading
- Mirth Connect: The Complete Guide →
- HL7 Integration: The Complete Guide →
- FHIR R4 Integration: The Complete Guide →
- Mirth Connect Installation Guide →
- Common Mirth Connect Issues & Fixes →
- Mirth Connect Java Heap Space Error →
- Mirth Connect MLLP Connection Refused →
- Mirth Connect Channel Not Starting →
- Mirth Connect Groovy vs JavaScript Transformers →
- Mirth Connect Performance Tuning →
- Mirth Connect Database Configuration →
- MLLP Protocol Explained →
- HL7 ACK/NAK Explained →
- HL7 ORM + ORU Lab Workflow →
- Mirth Connect SSL/TLS Hardening →
- Mirth FHIR Server →
- HL7 Integration Services — USA →