# HL7 v2 acknowledgments

What AA, AE and AR mean, what an ACK contains and who sees the error details.

Every HL7 v2 message is answered with an acknowledgment - an ACK. The sender does not consider a message delivered until the ACK arrives, and retransmits when none does. This page covers what each code means, what the ACK message contains and who sees the error details, in both directions - messages you [receive](https://zato.io/docs/dev/healthcare/hl7v2/mllp/index.html) and messages you [send](https://zato.io/docs/dev/healthcare/hl7v2/mllp/outgoing.html).

## The codes {#the-codes}

| Code | Meaning | When you see it |
| --- | --- | --- |
| **AA** | Application Accept | The message was processed successfully |
| **AE** | Application Error | The message itself is at fault - validation rejected its content or the application found it impossible to process. Resending the same message would not help |
| **AR** | Application Reject | The message was turned away for reasons unrelated to its content - nothing was configured to handle it, or the application failed while processing it. The sender should retry later |

The enhanced processing mode uses commit-level counterparts, each handled like its application-level equivalent - `CA` like `AA`, `CE` like `AE` and `CR` like `AR`.

## What an ACK contains {#what-an-ack-contains}

An ACK is itself a small HL7 v2 message:

- **MSH** - the sender and receiver of the original message, swapped, with a fresh timestamp and `ACK` as the message type
- **MSA** - the acknowledgment code and the control ID of the original message, echoed from its MSH-10 - this is how the sender matches the ACK to the message it acknowledges
- **ERR** - present when there are error details to report, with the details and a condition code from HL7 table 0357

## When you receive messages {#when-you-receive-messages}

[MLLP channels](https://zato.io/docs/dev/healthcare/hl7v2/mllp/index.html) generate acknowledgments automatically:

- **AA** - your service processed the message and returned nothing, so the channel answered with the standard accept
- **AE** - your service raised `HL7ApplicationError`, or the channel's **Validate each message** toggle was on and the message failed validation - the message is at fault and the sender must not resend it as-is
- **AR** - your service raised any other exception, or no channel's [match criteria](https://zato.io/docs/dev/healthcare/hl7v2/mllp/index.html#route-by-msh-fields) matched the message and no default channel exists - the sender retransmits the message

When a service determines that a message cannot be processed because of its content, it raises `HL7ApplicationError`, and the sender receives `AE` with the text of the exception:

```python
from zato.common.hl7.exception import HL7ApplicationError

if msg.pid.administrative_sex not in ('F', 'M', 'O', 'U'):
    raise HL7ApplicationError(f'Unexpected PID-8 `{msg.pid.administrative_sex}`')
```

Any other exception - a database that is down, a FHIR server that refused a connection - is answered with `AR`, so the sender resends the message once the cause is gone.

A duplicate message - one whose MSH-10 control ID was already seen within the [deduplication window](https://zato.io/docs/dev/healthcare/hl7v2/mllp/config.html#deduplication), when one is set - receives an **AA** immediately and your service is not invoked again.

If your service returns a string starting with `MSH`, that message is sent as the response instead of a generated ACK - this is how custom acknowledgments, including query responses, are produced.

### Who sees the error details {#who-sees-the-error-details}

The channel's **Return error details in NAK responses** toggle controls what an `AE` or `AR` contains. It is **off** by default:

- **Off** - the ERR segment holds a generic error message. Off is the production default - the error details stay in your own logs.
- **On** - the ERR segment contains the actual exception text. Turn this on during development and integration testing, when the people reading the ACKs are the ones fixing the errors.

Either way, the full error is recorded on your side - in the server logs and in [the audit log](https://zato.io/docs/dev/healthcare/audit-log.html), where the message and its acknowledgment are stored together.

## When you send messages {#when-you-send-messages}

Sending through [outgoing connections](https://zato.io/docs/dev/healthcare/hl7v2/mllp/outgoing.html) returns an `AckResult` that has the remote system's answer already interpreted:

| Field | Type | Meaning |
| --- | --- | --- |
| `ack_code` | str | The MSA-1 code from the remote system, e.g. `AA` |
| `is_accepted` | bool | `True` when the message was accepted (`AA` or `CA`) |
| `should_retry` | bool | `True` when the remote system asked for a retry (`AR` or `CR`) |
| `error_text` | str | Error details when the message was not accepted |
| `ack_text` | str | The acknowledgment exactly as it arrived, ER7 and all |

```python
# Send 'data', the ER7 message, through the outgoing connection ..
conn = self.mllp['EHR Main']
result = conn.send(data)

# .. and the result holds the remote system's answer.
if result.is_accepted:
    self.logger.info(f'Message accepted: {result.ack_code}')
else:
    self.logger.warning(f'Message rejected: {result.ack_code} ({result.error_text})')
```

An acknowledgment is only trusted when its MSA-2 echoes the control ID of the message it answers - a reply naming another message is reported in `error_text` instead.

The connection's **Wait for the acknowledgment** setting - 250 ms by default - controls how long the reply is waited for. When no reply arrives within it, the connection raises an exception that describes what went wrong, rather than returning a result.

## Finding an acknowledgment {#finding-an-acknowledgment}

[The audit log](https://zato.io/docs/dev/healthcare/audit-log.html) stores each message and its acknowledgment, in either direction, as one exchange, found by the MSH-10 control ID or by a patient identifier from the message itself.

## See also {#see-also}

- [Receiving over MLLP](https://zato.io/docs/dev/healthcare/hl7v2/mllp/index.html) - The channels that answer with the codes above
- [Sending over MLLP](https://zato.io/docs/dev/healthcare/hl7v2/mllp/outgoing.html) - Outgoing connections, self.mllp and delivery retries
- [The audit log](https://zato.io/docs/dev/healthcare/audit-log.html) - Every message and its acknowledgment stored as one exchange

## Learn more {#learn-more}

- [Healthcare interface engine](https://zato.io/docs/dev/healthcare/) - Clinical messages, FHIR, conversion and operations
- [HL7 v2 parsing](https://zato.io/docs/dev/healthcare/hl7/v2/parsing/) - Messages as typed objects, with field access and validation
- [MLLP channels](https://zato.io/docs/dev/healthcare/hl7v2/mllp/) - Receiving and sending HL7 v2 over MLLP sockets
- [FHIR integrations](https://zato.io/docs/dev/healthcare/hl7/fhir/) - Read and write FHIR resources, with paths, bundles and extensions
- [HL7 v2 to FHIR](https://zato.io/docs/dev/healthcare/hl7/to-fhir/) - Converting v2 messages into FHIR bundles, codes and references included
- [EDIFACT](https://zato.io/docs/dev/healthcare/edifact/) - Parsing interchanges, dialects and the transports they arrive on
- [Transformation](https://zato.io/docs/dev/healthcare/transformation/) - One message in, another standard out, in ordinary Python
- [Audit log](https://zato.io/docs/dev/healthcare/audit-log.html) - Every message with its acknowledgment, searchable by patient identifier
- [AI in clinical interfaces](https://zato.io/docs/dev/healthcare/ai/) - Services calling LLMs and AI agents calling clinical services as tools
