HL7 v2 Explained: The Messaging Standard Still Running Every Hospital in 2026
Introduction
This is the first article in my series on HL7 and healthcare interoperability. I originally wrote it in 2009, and it has been one of the most visited pages on this site ever since. A lot has changed in healthcare IT in the years between - FHIR arrived, cloud EHRs became normal, and regulators started mandating APIs - and yet the thing this article describes is still the single most common way clinical data moves between systems inside a hospital. So I have rewritten it from scratch for 2026: same fundamentals, but with the context an engineer needs today.
Who this is for: software developers, integration engineers, analysts and architects who are about to touch an HL7 v2 interface for the first time, or who have been handed one and want to understand what they are looking at. No prior healthcare knowledge is assumed.
The short version
- HL7 v2 is a text-based, event-driven messaging standard. When something happens in a clinical system (a patient is admitted, a lab result is ready), that system emits a message describing the event, and other systems consume it.
- Messages are made of segments (lines), which are made of fields separated by
|, which can be split further with^,~and&. - Every message is answered by an acknowledgement (ACK) so the sender knows whether it was accepted.
- Messages usually travel over TCP using a trivially simple framing protocol called MLLP, typically through an integration engine rather than point-to-point.
- FHIR did not replace v2. FHIR dominates APIs, apps and cross-organization exchange; v2 still dominates system-to-system feeds inside the hospital. You will need both.
“Time is the wisest counselor of all.” ~ Pericles
What HL7 v2 Is - and Why It Is Still Everywhere
"HL7" is Health Level Seven International, a standards development organization founded in 1987 and accredited by ANSI. The "seven" refers to the application layer (layer 7) of the OSI networking model - the standard concerns itself with the content and meaning of what is exchanged, not with the wires underneath. HL7 publishes several families of standards (v2, v3/CDA, FHIR); this article is about the oldest and most widely deployed of them, Version 2.x, which practitioners simply call "v2" or "HL7 2.x".
The problem v2 was created to solve has not gone away. A hospital runs dozens - often hundreds - of separate software systems: registration, scheduling, laboratory, radiology, pharmacy, billing, nursing, the electronic health record itself. Each was built by a different vendor at a different time. Before HL7, connecting any two of them meant a custom, proprietary interface. Version 2 gave everyone a common, vendor-neutral way to say "a patient was admitted" or "here is a lab result", and it did so in a format that was easy to produce and parse with the tools of the late 1980s: plain text with delimiters.
That simplicity is exactly why it survived. HL7 International itself estimates that v2 is in use at roughly 95% of US healthcare organizations, and the picture is similar in Canada, the UK, Australia and much of Europe. A modern hospital typically has somewhere between a few dozen and several hundred live v2 interfaces. They work, they are understood by the people who maintain them, and replacing them delivers little clinical value - so they stay.
Versions: why 2.3.1 and 2.5.1 are still the ones you meet
The standard has been revised many times, and each release is backward compatible by design: a receiver written for 2.3 can generally make sense of a 2.5 message because new fields are only ever added at the end of segments and existing ones are never renumbered. The practical consequence is that hospitals rarely upgrade.
| Version | Published | Why it matters today |
|---|---|---|
| 2.1 - 2.2 | 1990 - 1994 | Historical. You will occasionally see 2.2 on very old lab analyzers. |
| 2.3 / 2.3.1 | 1997 / 1999 | Still extremely common on legacy ADT and results feeds. |
| 2.4 | 2000 | Common in radiology and scheduling interfaces. |
| 2.5 / 2.5.1 | 2003 / 2007 | The de facto standard. US regulations (ONC certification, public health reporting, immunization registries) are largely written against 2.5.1. |
| 2.6 - 2.8 | 2007 - 2014 | Incremental. Seen mostly in newer public-health and lab implementation guides. |
| 2.9 | 2019 | Current release. Rare in production; useful as the most complete reference. |
Note that there was never an official "version 1" - it was a proof of concept that was superseded before it was published. If you are curious about the early history, René Spronk's account is the definitive one.
The Mental Model: Events, Messages, Segments, Fields
Everything in v2 flows from a single idea: a real-world event triggers a message. A patient is registered, admitted, transferred to another bed, or discharged. An order is placed. A result is finalized. Each of these is a trigger event, and for each trigger event the standard defines a message type - a specific list of segments, in a specific order, some required and some optional.
A message is therefore a hierarchy, and once you can see the hierarchy you can read any v2 message:
The five delimiters
The original version of this article only mentioned three of these. All five matter, and the two I left out (~ and \) are the ones that cause the most bugs in home-grown parsers.
| Character | Name | Separates | Example |
|---|---|---|---|
| | Field separator | Fields within a segment | PID|1||MRN123456 |
^ | Component separator | Components within a field | DOE^JANE^A (family^given^middle) |
~ | Repetition separator | Repeated values of one field | (217)555-0142^PRN^PH~[email protected]^NET |
\ | Escape character | Marks an escape sequence | \F\ is a literal pipe, \.br\ is a line break |
& | Sub-component separator | Sub-components within a component | ORD123&LabSystem |
Strictly speaking, these are only the defaults. The first two fields of every message declare which characters are in use: MSH-1 is the field separator itself (the character right after "MSH"), and MSH-2 lists the other four in the order component, repetition, escape, sub-component - which is why every message you will ever see begins with the odd-looking string MSH|^~\&|. A correct parser reads the delimiters from the message rather than assuming them. In practice almost nobody changes them, but "almost" is doing some work in that sentence.
A Real Message, Field by Field
Here is a complete ADT^A01 message - "Admit/Visit Notification". ADT stands for Admission, Discharge and Transfer, the family of messages that tracks where a patient is and who they are; A01 is the specific trigger event for an inpatient admission. This is the message a registration system sends to every other system in the hospital the moment a patient is assigned a bed.
MSH|^~\&|REGSYS|GENHOSP|LABSYS|GENHOSP|20260912083000||ADT^A01^ADT_A01|MSG00001|P|2.5.1
EVN|A01|20260912082500||||20260912082000
PID|1||MRN123456^^^GENHOSP^MR||DOE^JANE^A||19780415|F|||123 MAIN ST^^SPRINGFIELD^IL^62701^USA||(217)555-0142^PRN^[email protected]^NET^Internet|||M||ACCT0001
NK1|1|DOE^JOHN|SPO^Spouse^HL70063|123 MAIN ST^^SPRINGFIELD^IL^62701^USA|(217)555-0143
PV1|1|I|MED^301^A^GENHOSP||||1234^SMITH^ROBERT^^^DR|||MED||||7|||1234^SMITH^ROBERT^^^DR|IN|VISIT0001
Four segments carry almost everything (NK1, next of kin, is optional). Let us walk through them.
MSH - Message Header
Every message starts with MSH. It is the envelope: who sent it, who it is for, what it is, and how to reference it later.
| Field | Value | Meaning |
|---|---|---|
| MSH-1 | | | Field separator |
| MSH-2 | ^~\& | Encoding characters |
| MSH-3 / MSH-4 | REGSYS / GENHOSP | Sending application and sending facility |
| MSH-5 / MSH-6 | LABSYS / GENHOSP | Receiving application and receiving facility |
| MSH-7 | 20260912083000 | Date/time of message (YYYYMMDDHHMMSS, optionally with timezone) |
| MSH-9 | ADT^A01^ADT_A01 | Message type ^ trigger event ^ message structure |
| MSH-10 | MSG00001 | Message control ID - must be unique; the ACK will quote it back |
| MSH-11 | P | Processing ID: P = production, T = training, D = debugging |
| MSH-12 | 2.5.1 | Version of the standard the message claims to follow |
EVN - Event Type
EVN|A01|20260912082500||||20260912082000 repeats the trigger event (EVN-1), records when the event was recorded in the sending system (EVN-2) and when it actually occurred (EVN-6). The five-minute gap between those two timestamps is a realistic detail: a clerk admitted the patient at 08:20 and the system committed the record at 08:25. Downstream systems that care about "when did the patient actually arrive" must read EVN-6, not MSH-7.
PID - Patient Identification
PID is the segment you will read most often. The fields that matter most:
| Field | Value | Meaning |
|---|---|---|
| PID-3 | MRN123456^^^GENHOSP^MR | Patient identifier list. Component 1 is the ID, component 4 the assigning authority, component 5 the type (MR = medical record number). This field repeats, so a patient can carry an MRN, a health-card number and an enterprise ID all at once. |
| PID-5 | DOE^JANE^A | Patient name: family ^ given ^ middle. Also repeatable (legal name, alias, maiden name). |
| PID-7 | 19780415 | Date of birth |
| PID-8 | F | Administrative sex |
| PID-11 | 123 MAIN ST^^SPRINGFIELD^IL^62701^USA | Address: street ^ other ^ city ^ state ^ postal code ^ country |
| PID-13 | (217)555-0142^PRN^PH~[email protected]^NET^Internet | Home contact. Note the ~: two repetitions, a phone number and an email. |
| PID-18 | ACCT0001 | Patient account number (the billing encounter) |
PV1 - Patient Visit
PV1 describes the encounter: PV1-2 patient class (I = inpatient, O = outpatient, E = emergency), PV1-3 assigned location as point of care ^ room ^ bed ^ facility (MED^301^A^GENHOSP - medical ward, room 301, bed A), PV1-7 attending doctor, PV1-10 hospital service, PV1-14 admit source (7 = emergency room), PV1-17 admitting doctor, and PV1-19 the visit number that ties every later message about this stay together.
Notice how much of the message is empty: consecutive pipes with nothing between them. That is normal. The position of a field is its identity, so a sender that has nothing to say for PV1-4 through PV1-6 must still emit the separators so that PV1-7 lands in the right place. When you are debugging, count pipes carefully - an off-by-one here is the single most common integration bug.
Trigger Events and Message Families
There are hundreds of trigger events in the standard, but a handful of families account for the overwhelming majority of real-world traffic:
| Family | Examples | What it carries | Typical sender → receiver |
|---|---|---|---|
| ADT | A01 admit, A03 discharge, A04 register outpatient, A08 update patient, A40 merge patient | Patient demographics and encounter/location changes | Registration/EHR → everything else |
| ORM / OMG / OML | ORM^O01 general order, OML^O21 lab order | Orders for tests, medications, procedures | EHR/CPOE → lab, radiology, pharmacy |
| ORU | ORU^R01 unsolicited observation result | Lab results, vital signs, radiology reports, device data | Lab/radiology/devices → EHR |
| SIU | S12 new appointment, S13 reschedule, S15 cancel | Scheduling information | Scheduling → EHR, RIS, patient portals |
| DFT | P03 post detail financial transaction | Charges | Clinical systems → billing |
| MDM | T02 document notification and content | Transcribed/dictated documents | Transcription → EHR |
| VXU | V04 vaccination record update | Immunizations | EHR → public health registries |
| MFN | M02 staff/practitioner master file | Reference data (providers, locations) | EHR → downstream systems |
| QBP / RSP | Q22 find candidates | Queries and responses | Any → master patient index |
| ACK | - | Acknowledgement of any of the above | Receiver → sender |
Two things to take from this table. First, the same segment is reused everywhere: a lab result (ORU) and an admission (ADT) both carry a PID segment with identical structure, so the effort you invest in understanding PID pays off across every interface. Second, the message flow is asymmetric - orders flow out of the EHR, results flow back in, and ADT flows out to everyone. Almost every integration project is some combination of these three.
What an admission actually sets in motion
A single A01 fans out to every system that needs to know a patient has arrived. Each recipient does something different with it - and each one acknowledges it independently.
Acknowledgements: How the Sender Knows It Worked
v2 is built on a transmit-and-acknowledge model. For every message a receiver gets, it sends back an ACK message whose MSA segment says what happened. The A01 above would be answered with:
MSH|^~\&|LABSYS|GENHOSP|REGSYS|GENHOSP|20260912083001||ACK^A01^ACK|ACK00001|P|2.5.1
MSA|AA|MSG00001
Two things make this work. MSA-1 carries the acknowledgement code, and MSA-2 echoes the message control ID from MSH-10 of the original message - that is how the sender correlates the response to what it sent, which matters when messages are queued and answered out of order.
| MSA-1 | Meaning | What the sender should do |
|---|---|---|
| AA | Application Accept - processed successfully | Nothing. Done. |
| AE | Application Error - the message was understood but could not be processed (missing required field, unknown patient, business rule failed) | Fix the data and resend. Retrying the same message will fail the same way. |
| AR | Application Reject - the message could not be processed for reasons unrelated to content (wrong version, unsupported message type, receiver unavailable) | Investigate configuration. Often safe to retry later. |
A negative acknowledgement ("NACK" in everyday speech, though the standard never uses that word) should include an ERR segment saying what went wrong and where:
MSH|^~\&|LABSYS|GENHOSP|REGSYS|GENHOSP|20260912083001||ACK^A01^ACK|ACK00002|P|2.5.1
MSA|AE|MSG00002
ERR||PID^1^7|101^Required field missing^HL70357|E||||Date of birth (PID-7) is required
ERR-2 points at the offending location (segment PID, first occurrence, field 7), ERR-3 gives a coded reason from HL7 table 0357, ERR-4 the severity, and ERR-8 a human-readable message. A receiver that returns a bare AE with no ERR segment is technically compliant and practically useless; when you build one, be generous here.
Original mode versus enhanced mode
What I described above is original acknowledgement mode: one message, one application-level response. The standard also defines enhanced mode, controlled by MSH-15 (accept acknowledgement type) and MSH-16 (application acknowledgement type). In enhanced mode the receiver can first send a commit acknowledgement (codes CA, CE, CR) meaning "I have safely stored this message and will deal with it later", and then optionally an application acknowledgement once it actually has. Most interfaces you meet will use original mode, but if you see CA in an MSA segment, this is why.
Transport: MLLP and the Integration Engine
The v2 standard deliberately says nothing about how messages get from one system to another. Over the years they have been carried over files, FTP, SMTP, message queues and HTTP, but the overwhelmingly common transport is the Minimal Lower Layer Protocol (MLLP) over a persistent TCP connection. MLLP is about as minimal as its name suggests: it wraps each message in three control characters so the receiver can tell where one message ends and the next begins.
A start-block byte (vertical tab, 0x0B) precedes the message, every segment ends with a carriage return (0x0D), and an end-block byte (file separator, 0x1C) followed by a carriage return closes the message. The connection stays open; the receiver writes its ACK back the same way; the sender waits for that ACK before sending the next message. There is no authentication, no encryption and no compression in MLLP itself - it was designed for a trusted hospital LAN in 1990. Today you should expect to run it inside a VPN or, increasingly, wrap it in TLS (the standard defines this as "MLLP over TLS", and most engines support it natively).
Because every system needs to talk to many others, and each wants messages in a slightly different shape, almost no hospital wires systems together point-to-point. Instead, an integration engine (also called an interface engine) sits in the middle: Rhapsody, InterSystems HealthShare / Ensemble, Infor Cloverleaf, Mirth Connect and others. The engine receives each message once, transforms it per destination (rename a field, map a code set, drop segments the recipient cannot handle, add a Z-segment the recipient insists on), routes it, queues it if the destination is down, retries, and keeps an audit trail. When you are told "we have an HL7 interface", the practical reality is usually "we have a channel on the engine". If you write your own sender or receiver - and you will, when the engine cannot do what you need - the HAPI (Java) and NHAPI (.NET) articles in this series show you how.
The Parts Nobody Warns You About
Everything above is what the standard says. Here is what twenty years of building these interfaces says.
There is no such thing as a standard HL7 interface
The base standard is deliberately loose - it has to accommodate every hospital in the world - so almost every field is optional and many have several permissible formats. Each vendor, and often each hospital, therefore publishes an interface specification saying exactly which segments and fields they send or expect. Two "2.5.1 ADT" feeds from two vendors will differ in dozens of ways. The very first thing to ask for on any project is the other side's spec (and a set of real sample messages); the first thing to build is a validator against it. HL7 partially solved this with conformance profiles and, in the US, with regulatory implementation guides (for lab results, immunizations, syndromic surveillance and so on) that lock down what "2.5.1" means for that use case.
Z-segments
Any segment whose name begins with Z is locally defined - the standard reserves the letter for exactly this purpose. You will see ZPI, ZPV, ZIN and hundreds of others, each meaningful only to the system that invented it. They are the standard's official escape hatch, and every real feed has some.
Escape sequences and encoding
What happens when a patient's name contains an ampersand or a lab comment contains a pipe? The escape character. \F\ stands for the field separator, \S\ the component separator, \T\ the sub-component separator, \R\ the repetition separator, \E\ the escape character itself, and \.br\ a line break inside free text. A parser that does not un-escape these will silently corrupt data. Related: MSH-18 declares the character set, and the default is 7-bit ASCII. If you are handling names outside the English alphabet - which is to say, if you are handling names - make sure both sides agree on UNICODE UTF-8.
Dates, codes and the things that look simple
Timestamps are YYYYMMDDHHMMSS[.ssss][+/-ZZZZ], and every part after the year is optional, so 2026, 202609 and 20260912083000-0500 are all valid values of the same field. Coded values reference HL7 tables (the HL70063 in SPO^Spouse^HL70063 above) or external terminologies like LOINC and SNOMED CT - I cover that in a separate article on coded vocabularies. Patient identifiers change: a patient registered under a temporary ID in the emergency room will later be merged into their real record, announced via ADT^A40, and every downstream system must handle that merge or end up with duplicate patients.
HL7 v2 and FHIR: Where Each One Lives
The question every engineer asks in 2026 is why any of this still exists when FHIR is available. The honest answer is that they solve different problems and coexist in every hospital I know of.
| HL7 v2 | FHIR | |
|---|---|---|
| First released | 1989 | 2011 (draft); R4 in 2019 is the version in wide use |
| Paradigm | Event-driven messaging: “this just happened” | Resource-based REST API: “give me / update this thing” |
| Format | Pipe-delimited text | JSON or XML |
| Transport | MLLP over TCP, via integration engines | HTTPS, OAuth 2.0 / SMART on FHIR |
| Best at | High-volume, real-time feeds between systems inside an organization: ADT, orders, results | Apps, patient access, queries, cross-organization exchange, anything a web developer would build |
| Where you meet it | The hospital’s interface engine; lab analyzers; imaging; every legacy system | Patient portals, mobile apps, payer/provider exchange, regulatory APIs (US Cures Act, Canada’s pan-Canadian FHIR profiles, UK Core) |
| Tooling | HAPI HL7v2 (Java), NHAPI (.NET), integration engines | HAPI FHIR (Java), Firely SDK (.NET), every language |
The typical modern architecture is layered: v2 feeds continue to carry the clinical events between core systems, and a FHIR facade or repository sits on top exposing that data to apps and external partners. HL7 maintains an official v2-to-FHIR implementation guide mapping segments to resources (PID → Patient, PV1 → Encounter, OBX → Observation) precisely because so many organizations need to bridge the two. If you are entering this field now, plan to learn both - and learn v2 first, because it is where the data is.
Where to Go Next
This article is the foundation. From here, the material on this site branches by how deep you want to go:
- Concepts, chapter by chapter: my free 14-chapter HL7 v2 tutorial covers message structure, versions, common message types, validation, integration engines, testing and debugging, with quizzes at the end of each section.
- Write code in Java: the HAPI series walks through creating, sending, receiving, parsing, validating and acknowledging messages.
- Write code in .NET: the NHAPI series covers the same ground in C#.
- The neighbours: HL7 v3 and CDA, FHIR basics, DICOM for imaging, and IHE, which specifies how all of these are combined in real workflows.
- Test yourself: the HL7 quiz.
If you work with HL7, FHIR or DICOM and want to compare notes with other engineers doing the same, join the discussion in my LinkedIn group on healthcare interoperability engineering - questions, war stories and sample messages welcome.
You should read René Spronk's excellent article if you are interested in digging deeper into the beginnings of the HL7 standard, and Marc Kohli's video on radiology workflow remains one of the clearest explanations of how HL7 and DICOM fit together inside a hospital.