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.
| Header | Value |
|---|---|
X-Facility-ID | The facility UUID |
X-Webhook-Timestamp | Unix time in seconds (integer) |
X-Webhook-Signature | Lowercase hex HMAC-SHA256(secret, "{timestamp}.{rawBody}"), optionally prefixed sha256= |
- Body: the raw HL7 v2 message (
Content-Type: text/plain). A JSON body with amessage,hl7_message, ordatafield 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 (
401bad signature or stale timestamp,403EMR not enabled / no webhook secret,400unparseable HL7,429rate limited). Everything received and understood returns HTTP 200 with an HL7 ACK, and the outcome is in theMSA: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 treatARas 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
- Configuration Map (Settings → Configuration Map):
WARD_WEBHOOK_URL=https://<project-ref>.supabase.co/functions/v1/emr-inboundWARD_FACILITY_ID= the facility UUIDWARD_WEBHOOK_SECRET= the shared webhook secret Ward provides for this facility
- Import
mirth-adt-channel.xmland 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. - Deploy, send a test A01, and confirm a 200 with
MSA|AAand 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
| Symptom | Cause |
|---|---|
401 invalid_signature | Secret mismatch, or the signing input isn't exactly {timestamp}.{rawBody} (check for a re-encoded/normalized body) |
401 stale_timestamp | Engine host clock drift beyond 300s, or a queued message replaying an old timestamp; fix NTP and don't queue-and-retry |
| 403 not enabled | The facility EMR config is disabled, or no webhook secret is loaded |
| 200 `MSA | AA` but no patient appears |
| 200 `MSA | AR` |