Configuration
The Gateway section of appsettings.json, or environment variables (: becomes __). Keep secrets such as API keys, trigger URLs and certificates in Key Vault or environment variables, never in the file.
{
"Gateway": {
"ApiKeys": [], // from secrets: Gateway__ApiKeys__0
"RequireTls": false,
"AllowedDestinations": [ "emr.hospital.internal:2575", "10.20.*:*" ],
"Listeners": [
{
"Name": "adt",
"Port": 2575,
"BindAddress": "0.0.0.0",
"Encoding": "utf-8", // or "auto" to use MSH-18
"AcknowledgementMode": "Auto", // Auto | Workflow | None
"AcknowledgementType": "Original",
"BatchMode": "Split", // Split | Single | Reject
"ResponseTimeoutSeconds": 60,
"TimeoutBehavior": "SendNegativeAck",
"TargetUrl": "", // from secrets: Gateway__Listeners__0__TargetUrl
"HttpRetryCount": 2,
"Archive": false,
"Tls": { "Enabled": false, "Version": "1.2", "Certificate": "", "RequireClientCertificate": false }
}
]
}
}What the workflow receives
A POST with this JSON body, plus the headers X-MLLP-Message-Control-Id, X-MLLP-Connection-Id and X-MLLP-Message-Id (and X-MLLP-Batch-Control-Id for batches). The built-in trigger's outputs have the same fields.
{
"message": "MSH|^~\&|LAB|HOSP|EHR|HOSP|20261016093000||ADT^A01^ADT_A01|MSG00001|P|2.5.1\rPID|...",
"messageId": "…", "contextId": "…", "connectionId": "…", "sequenceNumber": 1,
"remoteEndpoint": "10.1.2.3:51234", "localEndpoint": "10.80.0.4:2575",
"receivedAt": "2026-10-16T09:30:00.0000000+00:00", "encoding": "utf-8",
"isValid": true, "validationError": null,
"messageControlId": "MSG00001", "messageType": "ADT^A01^ADT_A01", "messageCode": "ADT",
"triggerEvent": "A01", "messageStructure": "ADT_A01",
"sendingApplication": "LAB", "sendingFacility": "HOSP", "receivingApplication": "EHR", "receivingFacility": "HOSP",
"messageDateTime": "20261016093000", "processingId": "P", "versionId": "2.5.1",
"acceptAckType": "", "applicationAckType": "", "applicationAckRequested": true, "characterSet": "",
"isBatch": false, "tlsProtocol": null, "clientCertificateSubject": null, "clientCertificateThumbprint": null,
"archiveId": null
}How the response becomes the ACK
| Workflow response | Auto mode | Workflow mode |
|---|---|---|
| 2xx | AA (CA in enhanced mode) | The body is the ACK: plain HL7, a JSON string, or {"acknowledgement": "…"} |
| 400 or 422 | AR / CR | AR / CR |
| 5xx, 429, network error | Retried (HttpRetryCount, default 2, with backoff), then AE / CE | AE / CE, not retried, so a message never starts two runs |
| No response in time | AE / CE | TimeoutBehavior: SendNegativeAck, CloseConnection or NoResponse |
HTTP API
POST /api/v1/messages/send and POST /api/v1/acknowledgements back the custom connector used by Consumption Logic Apps. Both require an x-api-key, and the send API only connects to destinations listed in AllowedDestinations, so the gateway can't be used as an open relay. Client certificates are referenced by name from Gateway:ClientCertificates, so certificate material never travels in a request.
curl -X POST https://<gateway-fqdn>/api/v1/messages/send \
-H "x-api-key: <key>" -H "Content-Type: application/json" \
-d '{
"host": "emr.hospital.internal", "port": 2575,
"message": "MSH|^~\\&|LAB|HOSP|EMR|HOSP|20261016093000||ORU^R01^ORU_R01|MSG00042|P|2.5.1\rPID|...",
"waitForAck": true, "keepConnectionOpen": true, "failOnNegativeAck": true,
"useTls": true, "tlsTrustedServerThumbprints": [ "<sha-256 thumbprint>" ], "tlsClientCertificate": "emr"
}'Health
GET /healthz on port 8080 reports each listener's state, including why a listener didn't start (for example a plain listener when TLS is required).