Every HL7 interface eventually needs to be tested — against a parser, an integration engine, a destination system, or a unit-test harness. And every test needs a message. Writing one by hand is deceptively painful: forgetting MSH-2's encoding characters, miscounting pipes, mixing up MSH-5 and MSH-3, or leaving a required segment out entirely all cause silent rejections at the far end. This guide walks through the anatomy of a valid HL7 v2.x message, the most common trigger events you will need to test, and how to generate safe synthetic data without PHI risk.
The Anatomy of a Valid HL7 Message
An HL7 v2.x message is a pipe-delimited text document. The first segment is always MSH (Message Header), and its first two fields are unusual: MSH-1 is the field separator character itself (|), and MSH-2 declares the remaining encoding characters — component (^), repetition (~), escape (\), and subcomponent (&). Every standard message begins with exactly MSH|^~\&|. Miss this and no downstream parser can make sense of the rest.
Following MSH, trigger-event-specific segments carry the payload. ADT^A01 (patient admit) needs EVN, PID, and PV1 at minimum. ORU^R01 (lab result) needs PID, OBR, and OBX. Each segment has a rigid field order defined by the HL7 specification, and each field has a data type and cardinality (required, optional, repeating). The receiving parser validates these and rejects messages that deviate structurally.
The Test-Data Problem
Real messages carry Protected Health Information: patient names, medical record numbers, birthdates, physician identifiers, and institutional addresses. HIPAA (in the US) and equivalent data-protection regimes in Europe and elsewhere forbid sharing PHI outside the covered environment. This means the obvious testing shortcut — "just grab a message from production" — is not available. Integration engineers either hand-craft messages from the spec, copy templates from vendor documentation, or use a generator that emits structurally valid synthetic messages.
Synthetic Data Strategies
Good synthetic data follows three principles: (1) it is obviously fake, (2) it exercises realistic structure, and (3) it is deterministic when you need reproducibility. "Obviously fake" means a patient name like TEST^PATIENT^A, a date of birth of 19000101, a facility named SENDING_APP. Anyone glancing at a message with those values knows at a glance that it is test data, so no reviewer confuses it with production.
"Realistic structure" means the field lengths, cardinality, and data types match what a real message would carry. A PID-3 patient ID should look like a real MRN format — TEST000001^^^TESTFAC^MR, not abc. An OBX-5 lab value should be a plausible number with plausible units. The receiving system's validators are strict about types even when values are obviously fake.
"Deterministic" matters for regression tests. If your generator produces different values on every run, test assertions about specific fields break. The generator on this site uses fixed sample values so you can commit a generated message to a test fixture and it will match next week.
The Most Common Trigger Events to Test
ADT (Admit, Discharge, Transfer)
The ADT family is the most common reason an interface exists. Virtually every downstream clinical and administrative system depends on ADT feeds to keep its own patient roster synchronized. Test at minimum:
- ADT^A01 — Admit. New inpatient visit. Ensures new-patient registration and visit creation flows work end-to-end.
- ADT^A03 — Discharge. Ensures discharge cleanup, billing handoff, and bed-management updates trigger correctly.
- ADT^A04 — Register outpatient. Validates the outpatient registration path, which often has slightly different downstream routing than inpatient.
- ADT^A08 — Update patient info. The workhorse event. Any demographic correction, insurance change, or visit-detail update flows as an A08. It is also the most likely source of downstream bugs because downstream systems must reconcile updates against existing state.
- ADT^A11 — Cancel admit. Ensures prior-admit cancellations propagate; a common source of bed-management and billing discrepancies when mishandled.
Orders and Results (ORM, ORU)
ORM^O01 carries new orders — labs, imaging, pharmacy — to the performing system, and ORU^R01 carries results back. Testing should exercise:
- A single-OBX observation (one numeric result).
- A multi-OBX observation (a lab panel with 5–15 individual measurements).
- Abnormal flags (OBX-8) and result statuses (OBX-11: P, F, C for preliminary, final, corrected).
- Text results (OBX-2 = 'TX') for radiology and pathology reports.
Scheduling, Documents, Finance, Immunizations
SIU^S12 tests appointment creation — a common source of calendar-synchronization bugs. MDM^T02 tests clinical document transmission; BAR^P01 and DFT^P03 exercise the financial pipeline; VXU^V04 covers immunization reporting, which in several countries is now a mandatory public-health feed. A generator that supports all of these in a single interface speeds up QA for teams touching multiple domains.
Setting Processing ID Correctly
MSH-11, the Processing ID, tells downstream systems whether a message is production, training, or debug. Valid values are P (production), T (training), and D (debug). When injecting synthetic messages into a shared test environment, always set this to T or D. Most downstream systems have route filters that ignore non-P traffic for billing, prescribing, and reporting workflows — this is the mechanism that prevents a test lab result from appearing on a real patient's chart.
The generator on this site defaults to P because the generated message is meant as a structural reference. Change it to T before piping into a real integration engine.
Round-Tripping Through Parser and ACK
A generated message that looks right on screen can still fail a downstream parser. The cheapest validation is to round-trip the message through a parser of your own — ideally the same library your production system uses. On this site, the HL7 Viewer parses and highlights messages segment-by-segment, catching structural issues like missing required fields or wrong data-type encodings. The ACK Generator simulates the receiving system's response, exercising the acknowledgement path that is often forgotten in test suites.
Common Pitfalls
- Field separators in data. A name with an apostrophe or ampersand breaks if you don't escape it with
\F\,\S\, etc. Most generators escape for you; always test with at least one name containing an escape-worthy character. - MSH-7 time zones. The spec allows timezone offsets; some systems accept them, some don't. Prefer UTC without offset (
20260420120000) for maximum compatibility. - Missing PID-5 components. A name without at least family and given components (
SMITH^JOHN) fails some receivers even though the spec allows a single-component name. - Z-segments. Proprietary segments (Z-prefixed) are allowed by the spec but not standardized. Don't assume a downstream system will handle them — if your integration relies on a Z-segment, test it explicitly.
- Character encoding. HL7 v2 defaults to ASCII; UTF-8 is increasingly common but must be declared in MSH-18. Accented characters in PID-5 will look fine on your screen and land garbled downstream if the encoding isn't negotiated.
Using the Generator
Open the HL7 Message Generator, pick a message type, click 'Fill with sample data' to populate the required fields with obviously-fake values, and customize anything your test needs. The generated message updates live as you type. Use the Copy or Download buttons to pull the message into your test harness, or click 'Open in HL7 Viewer' to inspect it segment-by-segment before sending.
Every generated message parses cleanly through our HL7 parser — we verify this in the test suite — so the message you copy is guaranteed to be structurally valid. Combine it with the Viewer and ACK Generator and you have a complete loop for testing either side of an interface without touching real patient data.
Scaling Up: Automated Test Data Generation
For most interface QA workflows, a handful of hand-curated synthetic messages is enough. But when building automated regression suites for a large integration engine, or when certifying a new EHR connector, you may need hundreds or thousands of messages with varied patient profiles, edge-case field values, and mixed message types. The pattern that works well: use this generator as a template source, then wrap it in a small script (10-20 lines of Python or Node) that substitutes specific field values from a CSV or JSON fixture file. This gives you deterministic, version-controlled test data that matches the structural correctness the generator provides, without manually editing hundreds of files. Common parameterizations include patient name pools (TEST^SMITH, TEST^JONES, TEST^GARCIA), realistic but synthetic date ranges, and rotating MRN patterns that exercise different formats your integration might encounter from different feeding systems.
Compliance Context: HIPAA Safe Harbor and Synthetic Data
HIPAA's Safe Harbor rule identifies 18 specific data elements that must be removed or generalized for a dataset to be considered de-identified. Synthetic test data sidesteps this entire question because the data was never real to begin with — no Safe Harbor removal is required, no IRB approval is needed for internal testing, and no Business Associate Agreements must be in place with downstream test environments. This is why integration teams increasingly prefer synthetic generators over de-identified production extracts: the legal and procedural overhead is near zero, and the test coverage is often better because you can inject edge cases (long names, unicode addresses, future birthdates for pediatric patients) that may not exist in your production corpus. For certification testing against HL7-standard validation suites, synthetic data is the only option — certification bodies explicitly require non-PHI test messages.
Debugging Tips When a Generated Message Fails Downstream
Occasionally a generated message passes our parser but fails a downstream integration engine or vendor system. The most common causes, in order of frequency: (1) the receiver expects a specific Processing ID (MSH-11) — change from P to T or D, (2) the receiver expects a specific Sending Facility format (MSH-4) — replace SENDING_FAC with an ID the receiver recognizes from its configuration, (3) the receiver expects specific code-table values (e.g. Patient Class in PV1-2 must be I, O, E, P, R, or B from HL7 Table 0004, not your custom string), (4) the receiver enforces a specific version compatibility via MSH-12, (5) the message is missing a vendor-specific Z-segment. For case (5), the only fix is to consult the vendor's integration specification and hand-append the Z-segment to the generated message before sending.