The Envelope That Holds Everything Else
Almost every FHIR payload you will ever open is a Bundle. Search a server for a patient's observations and you get a Bundle. Post a set of related records in one atomic write and you send a Bundle. Receive a clinical document, a message, or a resource's version history, and it arrives as a Bundle. Yet the Bundle itself is one of the least-explained parts of the specification, because it is infrastructure rather than clinical content โ it carries no patient meaning of its own.
That is exactly why it deserves a careful read. A Bundle is a container with rules, and the rules change depending on one field: Bundle.type. A structure that is mandatory in a transaction is forbidden in a searchset. A field that means "here is where this resource lives" in one Bundle means "here is a temporary placeholder I invented" in another. Misreading those rules is a common source of integrations that appear to work in testing and fall apart against a real server.
This guide walks through what a Bundle is, the complete set of Bundle types with an emphasis on the four you will meet most often, the anatomy of an entry, and how to tell at a glance which kind of Bundle you are holding. You can open any example in the FHIR Bundle Explorer, which parses Bundles entirely inside your browser.
Bundle: A Resource Whose Job Is to Hold Resources
In FHIR R4, Bundle is a resource type like any other โ it has a resourceType of "Bundle", it can be stored and retrieved at a /Bundle endpoint, and it has its own defined elements. What makes it unusual is that its payload is other resources. The top-level structure, as defined on the R4 Bundle page, is short:
identifier(0..1) โ a persistent identifier for the Bundle itself, which stays the same as the Bundle is copied between servers.type(1..1) โ the only mandatory element. It declares what kind of Bundle this is and therefore which rules apply.timestamp(0..1) โ when the resources were assembled into this Bundle.total(0..1) โ the total number of matches, restricted to search and history Bundles.link(0..*) โ navigation links, each with arelationand aurl. This is how paging works.entry(0..*) โ the entries themselves, each wrapping one resource plus metadata about it.signature(0..1) โ a digital signature over the Bundle.
Note what is not there. There is no patient, no date of service, no author. A Bundle has no clinical semantics โ it is packaging. The meaning lives in the resources inside it and in the type that says how to interpret the packaging.
The Complete Set of Bundle Types
R4 defines nine codes for Bundle.type: document, message, transaction, transaction-response, batch, batch-response, history, searchset, and collection. Four of these โ searchset, transaction, collection, and document โ account for the overwhelming majority of what an integration engineer actually handles, and the rest are best understood in relation to them. transaction-response and batch-response are what a server sends back after processing a transaction or batch. history is the version history of a resource or of a server. message is FHIR's event-driven messaging envelope, with a MessageHeader as its first entry.
searchset โ The Bundle You Read
When you issue a FHIR search โ GET [base]/Observation?patient=Patient/123 โ the server responds with a Bundle whose type is searchset. This is the Bundle most developers meet first, and it is read-only from the client's point of view: the server built it, and you consume it.
Two elements matter here and only here. Bundle.total carries the total number of resources matching the search, which may be far larger than the number of entries in this particular page. And Bundle.entry.search carries per-entry search metadata: mode and score. The mode field is the important one โ it distinguishes an entry that actually matched your query from one the server pulled in for context because you asked for _include or _revinclude. If you consume every entry in a searchset as though it were a match, an _included Practitioner will silently become a false positive in your results.
Paging is expressed through Bundle.link. A paged searchset carries links with relations such as self, next, previous, first, and last. The correct way to walk a large result set is to follow the next link until it stops appearing, rather than to construct your own offset arithmetic โ the server's cursor may not be a simple offset, and constructing your own can silently skip or duplicate rows.
transaction โ The Bundle You Write
A transaction Bundle is the opposite: you build it and POST it to the server's base URL, and every entry describes an operation to perform. The defining guarantee, per the R4 RESTful API rules, is atomicity โ the server SHALL either accept all actions and return 200 OK with a transaction-response Bundle, or reject everything and return a 4xx/5xx response with a single OperationOutcome. There is no partial success. A batch Bundle looks structurally identical but drops that guarantee: each entry is processed independently, the overall HTTP status is 200 OK regardless of whether individual entries failed, and you must inspect each entry's response to learn what happened.
Because entries are instructions rather than just data, each one carries Bundle.entry.request, with these sub-elements: method, url, ifNoneMatch, ifModifiedSince, ifMatch, and ifNoneExist. The method is the HTTP verb โ POST to create, PUT to update, DELETE to remove, GET to read. The conditional headers map onto FHIR's conditional create/update/delete: ifNoneExist, for instance, expresses "create this only if no resource already matches this search," which is how you avoid duplicating a Patient you may or may not have sent before.
The specification also fixes the order in which a server processes a transaction, and this order is independent of the order of entries in the Bundle. Servers process DELETE interactions first, then POST, then PUT or PATCH, then GET or HEAD, and finally resolve any conditional references. This is why the spec states that the outcome SHALL NOT depend on the order of resources in the transaction, and why a given resource may only appear once by identity.
Why a transaction Bundle Looks Strange the First Time
Newcomers are often thrown by a transaction Bundle in which every fullUrl is a UUID and every reference points at one of those UUIDs. This is deliberate. When you are creating several interlinked resources in one go โ a Patient, an Encounter for that Patient, and an Observation from that Encounter โ none of them has a server-assigned id yet. You cannot write "reference": "Patient/123" because 123 does not exist. So you invent a placeholder, use it as the entry's fullUrl, and reference it from the other entries. When the server processes the POSTs and assigns real ids, it rewrites the matching references inside the same Bundle so the links survive. The mechanics of that rewriting, and what happens when a reference points at nothing, are covered in detail in How FHIR References Work.

collection โ The Bundle That Just Holds Things
A collection Bundle is the plainest of all: a set of resources with no processing semantics whatsoever. No request elements, no search metadata, no atomicity, no first-entry rule. It means "here is a pile of resources that belong together for some reason I am not encoding in the Bundle itself."
This makes it the natural format for test fixtures, sample data sets, exports for analysis, and any hand-assembled payload you want to hand to a colleague. It is also the type you should reach for when you are tempted to use transaction for something that is not actually a transaction โ labelling a static data file as a transaction invites a downstream system to POST it somewhere and start writing records.
document โ The Bundle That Is a Clinical Document
A document Bundle is a frozen, attestable clinical document โ FHIR's answer to CDA. It carries the strictest structural rules of any Bundle type, and the R4 Bundle page states them as invariants: a document Bundle SHALL have an identifier with both a system and a value, SHALL have a date, and its first entry SHALL be a Composition resource. That Composition is the document's spine: it declares the type of document, the subject, the author, the attester, and a set of sections whose entries point into the rest of the Bundle.
The message type carries a parallel rule โ a message Bundle's first entry SHALL be a MessageHeader. In both cases the "first entry is special" rule is what makes the Bundle interpretable as a whole rather than as a bag of parts.
Anatomy of an Entry
Every entry in every Bundle shares the same shape, though which parts are allowed depends on the Bundle type:
fullUrl(0..1) โ the absolute URL for the resource in this entry.resource(0..1) โ the resource itself.search(0..1) โmodeandscore. Only permitted in search Bundles.request(0..1) โmethod,url, and the conditional fields. Mandatory for batch, transaction, and history; prohibited otherwise.response(0..1) โstatus,location,etag,lastModified,outcome. Mandatory for batch-response, transaction-response, and history; prohibited otherwise.link(0..*) โ entry-level links, mirroring the Bundle-level link structure.
Those "mandatory here, prohibited there" rules are formal invariants on the Bundle resource, not style advice. A validator will flag a search element in a collection Bundle, and it will flag a transaction entry with no request.
fullUrl Is the Most Misunderstood Field in FHIR
fullUrl answers the question "what is this resource's identity?" โ and the answer depends on the Bundle type. In a searchset returned by a server, it is the resource's real, resolvable address: https://example.org/fhir/Patient/123. In a transaction you are about to POST, it is typically a urn:uuid: placeholder that exists only for the duration of that Bundle, because the resource has no server identity yet. In a collection it may be either, or absent.
The R4 invariants require that fullUrl values be unique within a Bundle, unless two entries share a URL and differ by meta.versionId โ which is precisely the case in a history Bundle, where the same resource appears at several versions. If you are generating Bundles and reusing the same fullUrl across distinct resources, you have created something a conformant consumer cannot interpret.
Telling Bundles Apart at a Glance
When you open an unfamiliar payload, a few structural tells identify it immediately, even before you read type:
| Signal | Almost certainly aโฆ |
|---|---|
total present, link with relation next | searchset (paged) |
Every entry has request.method | transaction or batch |
Every entry has response.status | transaction-response or batch-response |
Every fullUrl is a urn:uuid: | transaction being constructed client-side |
| First entry is a Composition | document |
| First entry is a MessageHeader | message |
| Bare entries, no request/response/search | collection |
These heuristics are useful precisely because type is buried at the top of a file that may be tens of megabytes of minified JSON, and because a mislabelled Bundle โ content that looks like a transaction but is typed collection โ is a real and common defect.
Where Bundles Stop and NDJSON Starts
Bundles have a natural size ceiling. Because a Bundle is a single JSON document, a consumer generally has to parse the whole thing before it can act on any of it, which means memory consumption scales with the size of the export. That is fine for a search page or a clinical document and unworkable for a population-scale extract of every Observation in a health system.
This is the reason the FHIR Bulk Data Access specification does not return Bundles at all. Its output is NDJSON โ one resource per line, with each file containing resources of only one type โ which a consumer can stream line by line at constant memory. If you are moving from single-patient integration to population-scale analytics, that shift is the main thing you need to internalise; FHIR Bulk Data Export Explained covers the $export flow end to end.
Working Through a Bundle in Practice
Reading a Bundle by eye stops scaling somewhere around the second screen of JSON. The practical questions โ what resource types are in here and how many of each, which resources point at which, is anything referencing something that is not in the file, what do all the Observations look like as a table โ are structural questions that are painful to answer with a text editor.
The FHIR Bundle Explorer is built for exactly those questions. It auto-detects what you paste โ a Bundle, a single bare resource, NDJSON from a bulk export, or a plain JSON array of resources โ and gives you a count per resource type, a reference map, and a flat table you can export as CSV. Repeated values can be joined into one cell with a semicolon or expanded into one row per repeat, so nothing is silently dropped. It reads up to 25 MB and 20,000 resources, and when a cap is reached it tells you exactly how many resources were not parsed rather than quietly truncating.
Two properties matter for healthcare work. First, it is read-only and local: references are resolved only against the resources present in what you pasted, never by fetching anything over the network, because a Bundle is patient data and quietly retrieving a referenced resource from an external server would leak it. Second, it is not a validator โ it will not tell you that a profile requires an element you omitted. For conformance checking, pair it with the FHIR Resource Validator.
Conclusion
A Bundle is a container whose behaviour is entirely determined by Bundle.type. A searchset is a page of results, complete with total, paging links, and per-entry search modes. A transaction is an atomic set of instructions, where each entry carries a request and placeholder identities get rewritten by the server. A collection is a plain pile of resources with no processing semantics. A document is a frozen clinical artefact anchored by a Composition as its first entry. Learn to read type, fullUrl, and the presence or absence of request, response, and search, and the structure of any FHIR payload becomes obvious within seconds โ and when it does not, open it in the FHIR Bundle Explorer and let the structure show itself, with the data never leaving your browser.