HL7 v2 segments map to FHIR R4 resources by event-to-state translation: PID → Patient, PV1 → Encounter, OBR → ServiceRequest + DiagnosticReport, OBX → Observation, ORC → ServiceRequest, AL1 → AllergyIntolerance, DG1 → Condition, PR1 → Procedure, SCH → Appointment, IN1 → Coverage. Wrap the resulting resources in a FHIR Bundle, cross-reference them by URL, and remember the three tensions — positional vs. named, inline vs. reference, message stream vs. resource model — that produce most real-world mapping bugs.
Why this reference exists
If you've worked in healthcare integration for any length of time, you've hit this problem. You have a system that speaks HL7 v2. You have another system that speaks FHIR R4. Somewhere between them you need to translate one to the other, and the official documentation reads like it was written for people who already know the answer.
This is the reference we wish existed when we started doing this work. Every common HL7 v2 segment, mapped to its FHIR R4 equivalent, with code examples you can adapt directly. Bookmark it. Send it to your team. Use it as the source of truth when someone on a standup says “wait, what's MSH supposed to map to?”
This guide assumes basic familiarity with both standards. If HL7 v2 segments are still new, start with HL7 v2 message structure explained, and for the FHIR side see the FHIR R4 integration complete guide. Backed by the engineers who deliver Mirth Connect support for US healthcare organizations.
Why HL7 v2 to FHIR mapping is harder than it looks
The official IHE and HL7 mapping documents will tell you that PID maps to Patient, OBX maps to Observation, and PV1 maps to Encounter. That's true but incomplete.
The real complexity isn't in the segment-to-resource mapping. It's in three things the docs gloss over.
First, HL7 v2 is positional and FHIR is named. PID-5 is the patient's name because it's in the fifth position of the PID segment, not because it has a label. FHIR's Patient.nameis named explicitly. Anywhere you have field-level transformations, you're translating between positional thinking and named-property thinking, and that's where most bugs live.
Second, HL7 v2 carries identity inline and FHIR carries identity by reference.In HL7, the patient's name appears in every message that mentions them. In FHIR, the patient is a separate resource and every message references them by URL. Translating from inline-identity to reference-identity means you need to either look up or create the patient resource before you can create the observation that points to them.
Third, HL7 v2 is a stream of messages and FHIR is a model of resources. A single HL7 v2 ORUmessage containing 5 lab results becomes, in FHIR, a Bundle containing 1 Patient + 1 Encounter + 1 ServiceRequest + 5 Observations + (potentially) 1 DiagnosticReport that references all the observations. The cardinality doesn't match.
Keep all three of these tensions in mind as you read the rest of this reference. Most mapping bugs trace back to one of them.
The mental model: messages vs. resources
Before getting into segment-by-segment detail, internalize this:
HL7 v2 message = a verb.“An admission happened.” “A lab result is ready.” “An appointment was scheduled.” The message is event-oriented.
FHIR resource = a noun.“Here is a patient.” “Here is an observation.” “Here is an encounter.” The resource is state-oriented.
When you map HL7 to FHIR, you are translating verbs into nouns. The event (“an admission happened”) becomes a set of resources (“here is the patient, here is the encounter, here is the practitioner involved”).
This is why every HL7 message typically becomes a FHIR Bundle — a collection of resources representing the state described by the event.
Message header — MSH, EVN
MSH (Message Header)does not map to a FHIR resource directly. Instead, it informs the Bundle's metadata.
MSH-7(Date/Time of Message) →Bundle.timestampMSH-9(Message Type) → informs the choice of Bundle type and what resources to includeMSH-10(Message Control ID) →Bundle.identifierMSH-3(Sending Application) →Bundle.meta.sourceor a custom extension
EVN (Event Type) appears in ADT messages and similarly informs the Bundle context.
EVN-2(Recorded Date/Time) →Encounter.period.startfor admissionsEVN-4(Event Reason Code) →Encounter.reasonCode
Patient segments — PID, PD1, NK1
PID (Patient Identification) maps to the Patient resource.
| HL7 v2 Field | FHIR Path | Notes |
|---|---|---|
| PID-3 (Patient Identifier List) | Patient.identifier | Use system to encode the assigning authority (MRN, SSN, etc.) |
| PID-5 (Patient Name) | Patient.name | Map family, given, prefix, suffix to corresponding HumanName fields |
| PID-7 (Date of Birth) | Patient.birthDate | Format YYYY-MM-DD |
| PID-8 (Administrative Sex) | Patient.gender | M→male, F→female, O→other, U→unknown |
| PID-11 (Patient Address) | Patient.address | Map to Address with use=home |
| PID-13 (Phone — Home) | Patient.telecom | system=phone, use=home |
| PID-14 (Phone — Business) | Patient.telecom | system=phone, use=work |
| PID-15 (Primary Language) | Patient.communication.language | Use BCP-47 codes |
| PID-16 (Marital Status) | Patient.maritalStatus | Map HL7 0002 to FHIR v3-MaritalStatus |
| PID-22 (Ethnic Group) | Patient.extension (us-core-ethnicity) | US Core extension |
| PID-10 (Race) | Patient.extension (us-core-race) | US Core extension |
PD1 (Patient Additional Demographics) extends Patient with additional info.
PD1-4(Primary Care Provider) →Patient.generalPractitioner(reference to Practitioner)
NK1 (Next of Kin) maps to either a separate RelatedPerson resource or to Patient.contact.
- For tight relationships (emergency contact, guardian) →
Patient.contact - For full relationship modeling →
RelatedPersonreferencing the Patient
Visit segments — PV1, PV2
PV1 (Patient Visit) maps to the Encounter resource.
| HL7 v2 Field | FHIR Path | Notes |
|---|---|---|
| PV1-2 (Patient Class) | Encounter.class | I→inpatient, O→outpatient, E→emergency |
| PV1-3 (Assigned Patient Location) | Encounter.location.location | Reference to Location resource |
| PV1-7 (Attending Doctor) | Encounter.participant.individual | Reference to Practitioner; type=ATND |
| PV1-8 (Referring Doctor) | Encounter.participant.individual | type=REF |
| PV1-17 (Admitting Doctor) | Encounter.participant.individual | type=ADM |
| PV1-19 (Visit Number) | Encounter.identifier | |
| PV1-44 (Admit Date/Time) | Encounter.period.start | |
| PV1-45 (Discharge Date/Time) | Encounter.period.end | |
| PV1-36 (Discharge Disposition) | Encounter.hospitalization.dischargeDisposition |
PV2 (Patient Visit Additional Info) provides additional context.
PV2-3(Admit Reason) →Encounter.reasonCodePV2-25(Visit Priority Code) →Encounter.priority
For the full ADT context that drives most PV1 mapping work, see our HL7 ADT messages complete reference.
Observation segments — OBR, OBX
This is where most lab integrations live. Understanding this section well will solve 60% of your real-world mapping problems.
OBR (Observation Request) maps to ServiceRequest (and informs DiagnosticReport).
| HL7 v2 Field | FHIR Path |
|---|---|
| OBR-2 (Placer Order Number) | ServiceRequest.identifier (type=PLAC) |
| OBR-3 (Filler Order Number) | ServiceRequest.identifier (type=FILL) |
| OBR-4 (Universal Service ID) | ServiceRequest.code |
| OBR-7 (Observation Date/Time) | DiagnosticReport.effectiveDateTime |
| OBR-14 (Specimen Received Date/Time) | Specimen.receivedTime |
| OBR-16 (Ordering Provider) | ServiceRequest.requester |
| OBR-22 (Results Rpt/Status Chng Date/Time) | DiagnosticReport.issued |
| OBR-25 (Result Status) | DiagnosticReport.status |
OBX (Observation/Result) maps to Observation.
| HL7 v2 Field | FHIR Path |
|---|---|
| OBX-2 (Value Type) | informs how to populate Observation.value[x] |
| OBX-3 (Observation Identifier) | Observation.code (typically LOINC) |
| OBX-5 (Observation Value) | Observation.value[x] — type depends on OBX-2 |
| OBX-6 (Units) | Observation.valueQuantity.unit |
| OBX-7 (References Range) | Observation.referenceRange.text |
| OBX-8 (Abnormal Flags) | Observation.interpretation |
| OBX-11 (Observation Result Status) | Observation.status |
| OBX-14 (Date/Time of Observation) | Observation.effectiveDateTime |
| OBX-19 (Date/Time of Analysis) | Observation.issued |
OBX-2 value type mapping:
NM(numeric) →Observation.valueQuantityST(string) →Observation.valueStringCE/CWE(coded entry) →Observation.valueCodeableConceptDT(date) →Observation.valueDateTimeTM(time) →Observation.valueTime
Order segments — ORC
ORC (Common Order) typically accompanies OBR and maps to additional ServiceRequest fields.
| HL7 v2 Field | FHIR Path |
|---|---|
| ORC-1 (Order Control) | informs ServiceRequest.status and intent |
| ORC-2 (Placer Order Number) | ServiceRequest.identifier (type=PLAC) |
| ORC-3 (Filler Order Number) | ServiceRequest.identifier (type=FILL) |
| ORC-5 (Order Status) | ServiceRequest.status |
| ORC-9 (Date/Time of Transaction) | ServiceRequest.authoredOn |
| ORC-12 (Ordering Provider) | ServiceRequest.requester |
ORC-1 (Order Control) to ServiceRequest.status mapping:
NW(New Order) → status=active, intent=orderCA(Cancel Order) → status=revokedDC(Discontinue) → status=revokedHD(Hold Order) → status=on-holdRL(Release Previous Hold) → status=active
Allergy segments — AL1
AL1 (Patient Allergy Information) maps to AllergyIntolerance.
| HL7 v2 Field | FHIR Path |
|---|---|
| AL1-2 (Allergen Type Code) | AllergyIntolerance.category |
| AL1-3 (Allergen Code/Mnemonic/Description) | AllergyIntolerance.code |
| AL1-4 (Allergy Severity Code) | AllergyIntolerance.reaction.severity |
| AL1-5 (Allergy Reaction Code) | AllergyIntolerance.reaction.manifestation |
| AL1-6 (Identification Date) | AllergyIntolerance.recordedDate |
Always link AllergyIntolerance back to the Patient via AllergyIntolerance.patient.
Diagnosis segments — DG1
DG1 (Diagnosis) maps to Condition.
| HL7 v2 Field | FHIR Path |
|---|---|
| DG1-3 (Diagnosis Code) | Condition.code (typically ICD-10) |
| DG1-5 (Diagnosis Date/Time) | Condition.recordedDate |
| DG1-6 (Diagnosis Type) | informs Condition.category |
| DG1-15 (Diagnosis Priority) | extension or Condition.severity |
For Encounter-related diagnoses, also populate Condition.encounter.
Procedure segments — PR1
PR1 (Procedures) maps to Procedure.
| HL7 v2 Field | FHIR Path |
|---|---|
| PR1-3 (Procedure Code) | Procedure.code (CPT or SNOMED) |
| PR1-5 (Procedure Date/Time) | Procedure.performedDateTime |
| PR1-11 (Procedure Priority) | extension |
| PR1-12 (Associated Diagnosis Code) | Procedure.reasonCode |
Scheduling segments — SCH, AIS, AIG, AIL, AIP
SIU messages carry appointment data and have a particularly complex translation. The high-level mapping:
SCH (Schedule Activity Information) maps to Appointment.
| HL7 v2 Field | FHIR Path |
|---|---|
| SCH-1 (Placer Appointment ID) | Appointment.identifier |
| SCH-3 (Filler Appointment ID) | Appointment.identifier |
| SCH-7 (Appointment Reason) | Appointment.reasonCode |
| SCH-11 (Appointment Timing Quantity) | Appointment.start, .end, .minutesDuration |
| SCH-25 (Filler Status Code) | Appointment.status |
AIS (Appointment Information — Service) adds the type of service.
AIS-3(Universal Service ID) →Appointment.serviceType
AIG (Appointment Information — General Resource), AIL (Location), and AIP (Personnel) each describe participants in the appointment. These all become Appointment.participant entries with different participant.type values.
Insurance segments — IN1, IN2, IN3
IN1 (Insurance) maps to Coverage.
| HL7 v2 Field | FHIR Path |
|---|---|
| IN1-2 (Insurance Plan ID) | Coverage.identifier |
| IN1-3 (Insurance Company ID) | Coverage.payor (reference to Organization) |
| IN1-12 (Plan Effective Date) | Coverage.period.start |
| IN1-13 (Plan Expiration Date) | Coverage.period.end |
| IN1-16 (Name of Insured) | Coverage.subscriber (Patient or RelatedPerson) |
| IN1-17 (Insured's Relationship to Patient) | Coverage.relationship |
| IN1-36 (Policy Number) | Coverage.subscriberId |
IN2 and IN3 carry additional detail and are typically merged into the same Coverage resource or extensions.
Z-segments and custom data
Z-segments — segments starting with Z, like ZPD or ZIN — are the wild card of HL7 v2. They carry custom data that no standard exists for.
The FHIR equivalent is extensions. Every Z-field becomes an extension on the most relevant resource. For example, if ZPD-3 carries a custom patient attribute, it becomes:
{
"resourceType": "Patient",
"extension": [
{
"url": "https://yourorg.org/fhir/StructureDefinition/zpd-3-custom-attribute",
"valueString": "..."
}
]
}Always define the StructureDefinitionfor any extension you create. Otherwise, you've moved the problem from “what does ZPD-3 mean” to “what does this extension mean” without solving anything.
The full message → bundle pattern
Putting it all together, here's the structure for a typical ORU^R01 message (lab result) translated to FHIR.
Source HL7 v2 message:
MSH|...|...|ORU^R01|...
PID|...
PV1|...
OBR|...
OBX|1|NM|...
OBX|2|NM|...
OBX|3|CE|...Destination FHIR Bundle:
{
"resourceType": "Bundle",
"type": "message",
"timestamp": "2026-05-12T10:23:45Z",
"identifier": { "value": "MSH-10 value here" },
"entry": [
{ "resource": { "resourceType": "MessageHeader", "..." : "..." } },
{ "resource": { "resourceType": "Patient", "...": "..." } },
{ "resource": { "resourceType": "Encounter", "...": "..." } },
{ "resource": { "resourceType": "ServiceRequest", "...": "..." } },
{ "resource": { "resourceType": "DiagnosticReport", "...": "..." } },
{ "resource": { "resourceType": "Observation", "...": "..." } },
{ "resource": { "resourceType": "Observation", "...": "..." } },
{ "resource": { "resourceType": "Observation", "...": "..." } }
]
}Resources reference each other via reference fields — Observation.subject points to Patient/[id], Observation.encounter points to Encounter/[id], and so on.
Common mistakes and how to avoid them
Six bugs we see in nearly every HL7 → FHIR pipeline we audit:
Mistake 1 — Treating Bundle as optional
New mappers sometimes try to send a single Observation resource directly instead of wrapping it in a Bundle. This fails most FHIR servers' validation because the receiving system has no context about the patient.
Mistake 2 — Hardcoding identifier systems
PID-3 carries identifiers from multiple authorities (MRN, SSN, payer IDs). Hardcoding "system": "urn:oid:1.2.3" means every patient ends up with the wrong identifier system. Map the HD component of PID-3 (the assigning authority) to the FHIR identifier.system.
Mistake 3 — Ignoring the difference between Patient.gender and PID-8
HL7 uses M/F/O/U. FHIR uses male/female/other/unknown. Don't pass through HL7's single-letter codes — FHIR validation will reject them.
Mistake 4 — Forgetting timezone information
HL7 v2 timestamps frequently omit timezone. FHIR's dateTimefields strongly prefer timezone-qualified ISO 8601. Decide on a default timezone (typically the sending facility's local time) and apply it consistently.
Mistake 5 — Creating duplicate Patient resources
Each new HL7 message contains the patient inline. If you create a new Patient resource for every message, you end up with thousands of duplicate Patients. Use conditional create (POST /Patient?identifier=...) or maintain a local identity map.
Mistake 6 — Using the wrong status mapping
OBX-11 has values like F (Final), C (Corrected), P (Preliminary). FHIR Observation.status uses different vocabulary (final, corrected, preliminary, amended). Always map through a translation table — don't pass the values through.
Tools and validation
Useful tools when building HL7 to FHIR mappings:
- Mirth Connect (NextGen Connect). Our weapon of choice. Built-in HL7 parser, JavaScript and Groovy transformers, and you can ship FHIR resources via the HTTP Sender. We have a full walkthrough in how to build an HL7 interface in Mirth Connect.
- HAPI FHIR validator. Free, available as both a web tool and a CLI. Validates FHIR resources against any StructureDefinition. Use it on every Bundle you produce.
- Inferno test suite.ONC's official suite for testing FHIR conformance, especially relevant if you need to certify against USCDI requirements.
- LinuxForHealth FHIR Converter.Open-source tool from IBM that does pre-built HL7 to FHIR conversions. Useful as a reference implementation even if you don't use it in production — you can compare your output to its output.
- FHIR Mapping Language (FML).A formal DSL for expressing FHIR transformations. Powerful but heavyweight — most teams don't need it for basic HL7 to FHIR work.
Next steps
If you're building a real HL7 to FHIR pipeline, the next pieces you'll need beyond this mapping reference are:
- A solid understanding of HL7 v2 message structure.
- Familiarity with HL7 ADT messages since they're the most common in real-world integration work.
- Working knowledge of FHIR bulk data if you're handling large datasets.
- A production-ready integration engine — Mirth Connect is what we recommend, and we've written about how to build an HL7 interface in Mirth Connect.
Want help building this in production?
This reference will get you 80% of the way there. The remaining 20% — the timezone edge cases, the deduplication strategy, the error handling, the throughput tuning — is where most real projects spend most of their time.
If you'd rather not figure that out from scratch, we do this work for a living. Book a 30-minute scoping call and we'll tell you what we'd build and what it would cost.
Or run our pricing calculator to estimate what an HL7 to FHIR project would cost based on your scope.
Prefer email? info@tactionsoft.com — we reply within 4 business hours.
Related Reading
- HL7 v2 Message Structure Explained →
- HL7 ADT Messages: Complete Reference →
- How to Build an HL7 Interface in Mirth Connect →
- FHIR Bulk Data ($export) Implementation Guide 2026 →
- Mirth Connect: The Complete Guide →
- HL7 Integration: The Complete Guide →
- FHIR R4 Integration: The Complete Guide →
- Mirth FHIR Server →
- Mirth Connect Pricing Calculator →