Skip to main content

Mirth ADT Channel Template

The hospital's interface engine owns MLLP framing and re-delivery. Ward's contract is one thing: a signed HTTPS POST to the emr-inbound function. This keeps the internal webhook contract stable no matter which engine (Mirth, Cloverleaf, Rhapsody) sits in front of it.

Download the template: mirth-adt-channel.xml (Mirth Connect 4.4+).

The webhook contract

emr-inbound authenticates every request by an HMAC, not by network trust, so a captured message cannot be replayed later.

HeaderValue
X-Facility-IDThe facility UUID
X-Webhook-TimestampUnix time in seconds (integer)
X-Webhook-SignatureLowercase hex HMAC-SHA256(secret, "{timestamp}.{rawBody}"), optionally prefixed sha256=
  • Body: the raw HL7 v2 message (Content-Type: text/plain). A JSON body with a message, hl7_message, or data field also works.
  • Timestamp skew: requests more than 300 seconds from server time are rejected, so the engine host must keep NTP-synced time. Because the signature is computed once per message, do not queue-and-retry a stale message (see setup); re-delivery belongs upstream at the MLLP sender.
  • Response: transport/auth failures are non-200 (401 bad signature or stale timestamp, 403 EMR not enabled / no webhook secret, 400 unparseable HL7, 429 rate limited). Everything received and understood returns HTTP 200 with an HL7 ACK, and the outcome is in the MSA:
    • MSA|AA — applied, parked for review, or a duplicate. Do not resend.
    • MSA|AE — business rejection (failed validation). Permanent; do not resend.
    • MSA|AR — a transient processing error. Resend (this is the retry signal, so a queued destination must treat AR as a failure to retry, not a success).
  • Idempotency: resends of an already-processed (facility, message control id) are no-ops, so at-least-once delivery from the engine is safe.

The signing transformer

The channel's destination transformer computes the signature Ward verifies. This is the only Ward-specific logic; everything else is standard Mirth. It reads the secret from the Configuration Map (never hard-code it in the channel):

var secret = configurationMap.get('WARD_WEBHOOK_SECRET');
// Integer unix seconds. Not String.valueOf(millis / 1000): Java double division renders scientific
// notation ("1.75E9"), which Ward rejects as a non-integer timestamp.
var timestamp = String(Math.floor(Date.now() / 1000));
var signingInput = timestamp + '.' + connectorMessage.getRawData();

var mac = Packages.javax.crypto.Mac.getInstance('HmacSHA256');
mac.init(new Packages.javax.crypto.spec.SecretKeySpec(
new java.lang.String(secret).getBytes('UTF-8'), 'HmacSHA256'));
var digest = mac.doFinal(new java.lang.String(signingInput).getBytes('UTF-8'));

var hex = new java.lang.StringBuilder();
for (var i = 0; i < digest.length; i++) {
hex.append(java.lang.Integer.toHexString((digest[i] & 0xFF) | 0x100).substring(1));
}
channelMap.put('wardTimestamp', timestamp);
channelMap.put('wardSignature', hex.toString());

The HTTP Sender then sends the raw message with X-Webhook-Timestamp: ${wardTimestamp} and X-Webhook-Signature: ${wardSignature}.

Setup

  1. Configuration Map (Settings → Configuration Map):
    • WARD_WEBHOOK_URL = https://<project-ref>.supabase.co/functions/v1/emr-inbound
    • WARD_FACILITY_ID = the facility UUID
    • WARD_WEBHOOK_SECRET = the shared webhook secret Ward provides for this facility
  2. Import mirth-adt-channel.xml and point the source MLLP listener at the ADT feed. The destination queue is off by design (the signature is per-message); if you enable it, sign per dispatch attempt.
  3. Deploy, send a test A01, and confirm a 200 with MSA|AA and a new patient in Ward.

Honoring MSA|AR (retry)

The template channel treats any HTTP 200 as delivered, so it does not act on an MSA|AR (Ward's "transient error, resend" signal). A rejected message is still visible in Ward's ADT log (and the raw message is retained 90 days), and a control-id retransmit reprocesses cleanly, but for automatic re-delivery add a response transformer on the destination that fails the message on AR:

// Fail the dispatch on a Ward MSA|AR so re-delivery kicks in (msg is the returned HL7 ACK).
if (msg['MSA']['MSA.1']['MSA.1.1'].toString() === 'AR') {
responseStatus = ERROR; // or QUEUED if you re-enable the destination queue with per-attempt signing
responseStatusMessage = 'Ward AR: ' + msg['MSA']['MSA.3']['MSA.3.1'].toString();
}

Map the source connector's response to this destination so the sending system sees the NAK and resends, or re-enable the destination queue (and move the signing into a per-attempt step, since a stale timestamp is rejected after 300s).

Troubleshooting

SymptomCause
401 invalid_signatureSecret mismatch, or the signing input isn't exactly {timestamp}.{rawBody} (check for a re-encoded/normalized body)
401 stale_timestampEngine host clock drift beyond 300s, or a queued message replaying an old timestamp; fix NTP and don't queue-and-retry
403 not enabledThe facility EMR config is disabled, or no webhook secret is loaded
200 `MSAAA` but no patient appears
200 `MSAAR`