A FHIR Bundle is the resource that carries a set of other resources in one payload, and its type decides the rules: a searchset returns search results, a transaction writes resources as one atomic unit, a batch runs independent actions, and document and message Bundles package clinical content for exchange. Every FHIR API you call answers in Bundles, so how your code reads and writes them decides whether a pipeline is correct or quietly loses data.
This guide covers all nine R4 (4.0.1) Bundle types. The JSON below was captured from requests we ran against the public HAPI FHIR R4 test server, then trimmed for length. Every rule is checked against the R4 specification pages for Bundle, the RESTful API and Search. It is written for engineering leads doing EHR integration who need to page through results, write linked resources safely and know what a server supports before building on it.
Key takeaways
Bundle.typeis required, has nine values in R4, and controls which entry elements are allowed:search,requestorresponse.- In a searchset,
Bundle.totalcounts matches only, never includes, and it is optional. The public HAPI server left it out until we sent_total=accurate. - Page by following the
nextlink exactly as the server returns it. Page links are opaque, and includes can repeat from one page to the next. - A transaction is all or nothing and a failed one returns a single OperationOutcome. A batch returns HTTP 200 with a status per entry, so partial failure is normal.
- Link new resources with
urn:uuid:fullUrls and putifNoneExiston creates so a retry cannot duplicate data. - Never assume a server accepts transaction or batch. Read
rest.interactionin its CapabilityStatement, then test with a read-only batch.
What is a FHIR Bundle?
A FHIR Bundle is a container resource for moving or storing a group of resources together. R4 uses it for search results, version history, messages, clinical documents and multi-resource writes. Unlike contained resources, every resource in a Bundle keeps its own identity and can also exist on its own on a server.
That separates a Bundle from contained resources, which only mean something inside their parent, and from List, Group and Composition, which reference resources and carry business meaning. Bundle.type is 1..1 with a required binding, and through invariants bdl-1 to bdl-4 it controls which entry elements may appear, which is where most validator errors come from. New to the resource model? Start with what FHIR is and how it works.
What are the FHIR Bundle types in R4?
R4 defines nine Bundle types: searchset, history, transaction, transaction-response, batch, batch-response, document, message and collection. Two carry read results, four are the request and response halves of multi-action calls, and three package resources for exchange or storage. The table shows direction, typical use and the rule that matters most.
| Bundle.type | Direction | Used for | Key rules |
|---|---|---|---|
searchset | Server to client | Results of a search or an operation | Only type allowed to carry entry.search (bdl-2). total allowed (bdl-1). |
history | Server to client | Results of _history on an instance, type or system | Every entry has request and response (bdl-3, bdl-4). Oldest versions last. |
transaction | Client to server | Atomic write of linked resources | entry.request required. All entries succeed or all fail. |
transaction-response | Server to client | Result of a successful transaction | entry.response required, one per request entry, same order. |
batch | Client to server | Independent reads or writes in one HTTP call | entry.request required. No interdependencies between entries. |
batch-response | Server to client | Per-entry results of a batch | May mix success and failure statuses. |
document | Sender to receiver, or stored | Clinical documents such as discharge summaries | Composition first (bdl-11), identifier system and value (bdl-9), timestamp (bdl-10). |
message | Sender to receiver | Event-driven messaging | MessageHeader first (bdl-12). |
collection | Any | Ad hoc packages, test fixtures, file drops | No processing rules beyond persistence. |
Anatomy of a FHIR Bundle: which elements matter
A Bundle has a handful of top-level elements (type, identifier, timestamp, total, link, signature) and a list of entries. Each entry can hold a fullUrl and a resource, plus only the metadata its Bundle type allows: search details in searchsets, request details in writes and history, and response details in results.
| Element | Card. | What it holds | When it appears |
|---|---|---|---|
type | 1..1 | One of the nine codes above | Always |
identifier | 0..1 | Persistent id that survives copying between servers | Required for documents (bdl-9) |
timestamp | 0..1 | When the Bundle was assembled | Required for documents (bdl-10) |
total | 0..1 | Number of match entries across all pages | Only searchset and history (bdl-1) |
link | 0..* | relation plus url: self, first, previous, next, last | Mostly searchset and history |
entry.fullUrl | 0..1 | Absolute URL or urn:uuid: identity of the resource | Must not be version-specific (bdl-8); unique unless versions differ (bdl-7) |
entry.resource | 0..1 | The resource itself | Required unless the entry has a request or response (bdl-5) |
entry.search | 0..1 | mode (match, include, outcome) and score | Searchset only (bdl-2) |
entry.request | 0..1 | method, url, ifNoneMatch, ifModifiedSince, ifMatch, ifNoneExist | Required in batch, transaction and history (bdl-3) |
entry.response | 0..1 | status, location, etag, lastModified, outcome | Required in batch-response, transaction-response and history (bdl-4) |
The fullUrl rules deserve a second read. Outside transactions and batches, every entry needs a fullUrl, and it is the resource's identity rather than a versioned link, so it never contains /_history/. A resource without a server identity gets a urn:uuid: value. When a reader resolves a reference, the spec says to look inside the Bundle by fullUrl first and only then try the URL externally. If two entries match, the reference is ambiguous.
How does a FHIR searchset Bundle work?
A searchset Bundle is what a server returns for a search. Entries with search.mode of match satisfy your criteria, include entries were added by _include or _revinclude, and outcome entries carry warnings. Results arrive in pages, and the only correct way to the next page is the server's next link.
Here is a real search. We asked the public HAPI R4 server for two Encounters per page, with each Encounter's subject Patient included:
curl -H "Accept: application/fhir+json" \
"https://hapi.fhir.org/baseR4/Encounter?_include=Encounter:subject&_count=2" The response, trimmed (resource metadata, narrative and demographics removed):
{
"resourceType": "Bundle",
"id": "b95c0b08-ac46-4507-9685-db3d0b9348c1",
"type": "searchset",
"link": [
{ "relation": "self",
"url": "https://hapi.fhir.org/baseR4/Encounter?_count=2&_include=Encounter%3Asubject" },
{ "relation": "next",
"url": "https://hapi.fhir.org/baseR4?_getpages=b95c0b08-ac46-4507-9685-db3d0b9348c1&_getpagesoffset=2&_count=2&_pretty=true&_include=Encounter%3Asubject&_bundletype=searchset" }
],
"entry": [
{
"fullUrl": "https://hapi.fhir.org/baseR4/Encounter/137202491",
"resource": {
"resourceType": "Encounter",
"id": "137202491",
"status": "finished",
"class": { "code": "OPD" },
"subject": { "reference": "Patient/137202428" }
},
"search": { "mode": "match" }
},
{
"fullUrl": "https://hapi.fhir.org/baseR4/Encounter/137202496",
"resource": {
"resourceType": "Encounter",
"id": "137202496",
"status": "in-progress",
"class": {
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
"code": "EMER"
},
"subject": { "reference": "Patient/137202485" }
},
"search": { "mode": "match" }
},
{
"fullUrl": "https://hapi.fhir.org/baseR4/Patient/137202485",
"resource": {
"resourceType": "Patient",
"id": "137202485",
"gender": "male"
},
"search": { "mode": "include" }
}
]
} Four details in this one response break production code:
- There is no
total. It is optional and_totalis only a hint. With_total=accuratethis server reported 17,629 matching Encounters when we ran it. When present, total counts matches across all pages, never includes, and it is not the page size. - Two matches, one include. The first Encounter's subject, Patient/137202428, returned
410 Goneon a direct read. The spec says a referenced resource that cannot be retrieved is simply left out, with no error. - The next link is opaque. HAPI's is a server-side cursor (
_getpagesplus an offset). The Search page says clients must use the server supplied links to move between pages. - Includes repeat across pages. Page 2 held two matches and two includes, and Patient/137202485 was on page 1 too. Each page should carry the includes for its own matches, so deduplicate includes by fullUrl.
_count asks for a page size, and the server may return fewer entries than you asked for, so a short page is not proof of the last page. Only a missing next link means you are done. _count=0 is treated like _summary=count. A search can start as a POST, but follow-up pages are GET requests to the links. A minimal paging loop:
import requests
def fetch_all(url, headers):
matches, includes, warnings = [], {}, []
while url:
bundle = requests.get(url, headers=headers, timeout=60).json()
for entry in bundle.get("entry", []):
mode = entry.get("search", {}).get("mode", "match")
if mode == "match":
matches.append(entry["resource"])
elif mode == "include":
includes[entry["fullUrl"]] = entry["resource"]
else:
warnings.append(entry.get("resource"))
url = next((l["url"] for l in bundle.get("link", [])
if l["relation"] == "next"), None)
return matches, includes, warnings For the query side (chained parameters, _revinclude costs and sort order), see the 10 FHIR search patterns developers get wrong.
Transaction vs batch: what is the difference?
A transaction Bundle is all or nothing: every entry succeeds, or the server rejects the whole Bundle and changes nothing. A batch Bundle processes each entry on its own, returns HTTP 200 even when some entries fail, and reports a status per entry. Use a transaction for linked writes and a batch for independent work.
| Behavior | transaction | batch |
|---|---|---|
| Atomicity | All actions succeed or fail together | Each entry is treated as its own interaction |
| HTTP status | 200 on success; 400 or 500 type on failure | 200 if the batch was processed, whatever the entry outcomes |
| Response body | transaction-response Bundle, or one OperationOutcome on failure | batch-response Bundle with a status per entry |
| References between new entries | Allowed, including circular; the server rewrites them | Non-conformant |
Conditional references (Patient?identifier=...) | Allowed, and only here | Not allowed |
| Processing order | DELETE, POST, PUT or PATCH, GET or HEAD, then conditional references | Same order, but entries must not depend on each other |
| Best for | Patient, Encounter and Observations written as one event | Many unrelated reads, or independent updates |
Batch partial failure is easy to miss because the HTTP layer reports success. We sent three reads in one batch, and one asked for a Patient that does not exist:
{
"resourceType": "Bundle",
"type": "batch",
"entry": [
{ "request": { "method": "GET", "url": "Patient/137202485" } },
{ "request": { "method": "GET", "url": "Patient/does-not-exist-bdl-guide" } },
{ "request": { "method": "GET", "url": "Encounter?subject=Patient/137202485&_count=1" } }
]
} The server answered HTTP 200. Inside, trimmed:
{
"resourceType": "Bundle",
"type": "batch-response",
"entry": [
{ "resource": { "resourceType": "Patient", "id": "137202485" },
"response": { "status": "200 OK" } },
{ "response": {
"status": "404 Not Found",
"outcome": {
"resourceType": "OperationOutcome",
"issue": [ { "severity": "error", "code": "exception",
"diagnostics": "HAPI-2001: Resource Patient/does-not-exist-bdl-guide is not known" } ]
} } },
{ "resource": { "resourceType": "Bundle", "type": "searchset", "total": 2 },
"response": { "status": "200 OK" } }
]
} A client that checks only the outer status records the missing Patient as fetched. The search entry came back as a nested searchset Bundle.
A transaction behaves the opposite way. We sent one that created a Patient and an Observation whose subject was a conditional reference, Patient?identifier=https://example.org/fhir/mrn|NO-SUCH-MRN-BDL. This server does not accept inline match URLs, so it rejected the whole Bundle with HTTP 400:
{
"resourceType": "OperationOutcome",
"issue": [ {
"severity": "error",
"code": "processing",
"diagnostics": "HAPI-2282: Inline match URLs are not supported on this server. Cannot process reference: \"Patient?identifier=https://example.org/fhir/mrn|NO-SUCH-MRN-BDL\""
} ]
} A follow-up search for the Patient's family name returned a total of 0, so the valid Patient entry was rolled back too. Sent as a batch, the same two entries produced 201 Created for the Patient and 400 Bad Request for the Observation, leaving a Patient with no Observation. That is the whole trade-off in one experiment.
How to build a FHIR transaction Bundle with urn:uuid and conditional create
Give each new resource a temporary urn:uuid: fullUrl, point references at those values, and set request.ifNoneExist on each create so a retry cannot duplicate data. The server assigns real ids, rewrites every matching reference in the Bundle, and returns the new locations in a transaction-response.
R4 fixes the processing order so the outcome never depends on the order of entries:
- Process any DELETE interactions.
- Process any POST interactions.
- Process any PUT or PATCH interactions.
- Process any GET or HEAD interactions.
- Resolve any conditional references.
If resource identities overlap in steps 1 to 3, the transaction fails. Here is the Bundle we sent (synthetic data, example identifier systems):
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"fullUrl": "urn:uuid:b97f0c95-47ca-45f0-b6a4-6765811cdaf5",
"resource": {
"resourceType": "Patient",
"identifier": [ { "system": "https://example.org/fhir/mrn", "value": "BDL-fc1816" } ],
"name": [ { "family": "Testpatient", "given": [ "Bundle" ] } ],
"gender": "female",
"birthDate": "1990-01-01"
},
"request": {
"method": "POST",
"url": "Patient",
"ifNoneExist": "identifier=https://example.org/fhir/mrn|BDL-fc1816"
}
},
{
"fullUrl": "urn:uuid:fda820b5-18ff-4286-8cef-e5047b3eaa41",
"resource": {
"resourceType": "Encounter",
"identifier": [ { "system": "https://example.org/fhir/visit", "value": "V-fc1816" } ],
"status": "finished",
"class": {
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
"code": "AMB"
},
"subject": { "reference": "urn:uuid:b97f0c95-47ca-45f0-b6a4-6765811cdaf5" },
"period": { "start": "2026-07-20T09:00:00Z", "end": "2026-07-20T09:30:00Z" }
},
"request": {
"method": "POST",
"url": "Encounter",
"ifNoneExist": "identifier=https://example.org/fhir/visit|V-fc1816"
}
},
{
"fullUrl": "urn:uuid:10c78423-d6a9-471f-b97a-3632af16f0a4",
"resource": {
"resourceType": "Observation",
"identifier": [ { "system": "https://example.org/fhir/obs", "value": "HR-fc1816" } ],
"status": "final",
"category": [ { "coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "vital-signs" } ] } ],
"code": { "coding": [ {
"system": "http://loinc.org", "code": "8867-4", "display": "Heart rate" } ] },
"subject": { "reference": "urn:uuid:b97f0c95-47ca-45f0-b6a4-6765811cdaf5" },
"encounter": { "reference": "urn:uuid:fda820b5-18ff-4286-8cef-e5047b3eaa41" },
"effectiveDateTime": "2026-07-20T09:10:00Z",
"valueQuantity": {
"value": 72, "unit": "beats/minute",
"system": "http://unitsofmeasure.org", "code": "/min" }
},
"request": {
"method": "POST",
"url": "Observation",
"ifNoneExist": "identifier=https://example.org/fhir/obs|HR-fc1816"
}
}
]
} curl -X POST "https://hapi.fhir.org/baseR4" \
-H "Content-Type: application/fhir+json" \
-H "Prefer: return=minimal" \
--data @transaction.json The first run returned HTTP 200 and this transaction-response (trimmed; HAPI also attaches an informational OperationOutcome to each entry):
{
"resourceType": "Bundle",
"type": "transaction-response",
"entry": [
{ "response": { "status": "201 Created", "location": "Patient/138554300/_history/1", "etag": "1" } },
{ "response": { "status": "201 Created", "location": "Encounter/138554301/_history/1", "etag": "1" } },
{ "response": { "status": "201 Created", "location": "Observation/138554302/_history/1", "etag": "1" } }
]
} Sending the identical Bundle a second time returned:
{
"resourceType": "Bundle",
"type": "transaction-response",
"entry": [
{ "response": { "status": "200 OK", "location": "Patient/138554300/_history/1", "etag": "1" } },
{ "response": { "status": "200 OK", "location": "Encounter/138554301/_history/1", "etag": "1" } },
{ "response": { "status": "200 OK", "location": "Observation/138554302/_history/1", "etag": "1" } }
]
} Each conditional create found one match, so the server skipped the create and returned 200 OK with the existing location. The R4 rule: no match creates, one match returns 200 OK, multiple matches return 412 Precondition Failed. Reading the Observation back shows the rewrite: subject Patient/138554300, encounter Encounter/138554301. R4 marks conditional create, update, patch and delete as trial use, and not every server implements them.
Servers rewrite matching values in references, resource ids, uri, url, oid and uuid elements, and narrative href or src attributes, but not canonical elements. The Observation here is a minimal vital sign; our FHIR Observation resource guide covers what US Core expects.
For existing resources, ifMatch makes an update version-aware and a conditional update puts search criteria in the URL. These request elements are illustrative:
[
{ "method": "PUT", "url": "Patient/138554300", "ifMatch": "W/\"1\"" },
{ "method": "PUT", "url": "Patient?identifier=https://example.org/fhir/mrn|BDL-fc1816" }
] A conditional update with no match creates, with one match updates, and with multiple matches returns 412. The spec names the use case: a stateless client such as an interface engine that does not track server ids. That is how HL7 v2 feeds typically load through engines like Mirth Connect. Our HL7v2 to FHIR R4 Bundle mapping guide shows the mapping tables, and building a FHIR R4 server from scratch covers the server side.
What goes in a FHIR document Bundle and a message Bundle?
A document Bundle is an immutable clinical document: a Composition must be the first entry, and the Bundle needs an identifier with a system and value plus a timestamp. A message Bundle must start with a MessageHeader that names the event. Neither is executed like a transaction when you store it on a FHIR server.
Document Bundles
Rules from the R4 Documents page and the Bundle invariants:
- Composition is the first entry (bdl-11).
Bundle.identifierhas a system and value (bdl-9) and is never reused.Bundle.timestampis set (bdl-10). - Resources referenced from the Composition's subject, encounter, author, attester, custodian, event detail and section author, focus and entries SHALL be included.
- Nothing else goes in except a Binary stylesheet and Provenance, and the assembled document is immutable.
An illustrative skeleton, not captured from a server:
{
"resourceType": "Bundle",
"identifier": {
"system": "urn:ietf:rfc:3986",
"value": "urn:uuid:0c3151bd-1cbf-4d64-b04d-cd9187a4c6e0"
},
"type": "document",
"timestamp": "2026-07-20T10:00:00Z",
"entry": [
{
"fullUrl": "urn:uuid:5d1a6c7e-2f0b-4d8e-9a51-3c2b1e0f7a10",
"resource": {
"resourceType": "Composition",
"status": "final",
"type": { "coding": [ {
"system": "http://loinc.org", "code": "18842-5", "display": "Discharge summary" } ] },
"subject": { "reference": "urn:uuid:8e2f4b6a-1c3d-4e5f-8a9b-0c1d2e3f4a5b" },
"date": "2026-07-20T10:00:00Z",
"author": [ { "reference": "urn:uuid:3a4b5c6d-7e8f-4a1b-9c2d-e3f4a5b6c7d8" } ],
"title": "Discharge summary"
}
},
{
"fullUrl": "urn:uuid:8e2f4b6a-1c3d-4e5f-8a9b-0c1d2e3f4a5b",
"resource": { "resourceType": "Patient" }
},
{
"fullUrl": "urn:uuid:3a4b5c6d-7e8f-4a1b-9c2d-e3f4a5b6c7d8",
"resource": { "resourceType": "Practitioner" }
}
]
} In US Core, clinical notes are exposed through DocumentReference rather than as document Bundles, which changes how you retrieve them. Our guide to FHIR DocumentReference for clinical notes covers that path.
Message Bundles
From the R4 Messaging page:
- MessageHeader is the first entry (bdl-12) and names the event. Responses are message Bundles whose MessageHeader quotes the request id in
response.identifier. - A resend keeps its
MessageHeader.id, but aBundle.idis never reused. - Transport is out of scope (file, HTTP, MLLP, queues). On a REST server,
$process-messageis what processes a message.
What does a FHIR history Bundle contain?
A history Bundle lists versions of one resource, a resource type or a whole server, newest first, including deletes. Each entry carries a request showing the interaction that produced that version and a response with its status and ETag, so a subscriber can tell creates from updates and replay changes in order.
Captured from GET https://hapi.fhir.org/baseR4/Patient/137202485/_history?_count=2, trimmed:
{
"resourceType": "Bundle",
"type": "history",
"total": 12,
"link": [
{ "relation": "self",
"url": "https://hapi.fhir.org/baseR4/Patient/137202485/_history?_count=2" },
{ "relation": "next",
"url": "https://hapi.fhir.org/baseR4/Patient/137202485/_history?_count=2&_offset=2" }
],
"entry": [
{
"fullUrl": "https://hapi.fhir.org/baseR4/Patient/137202485",
"resource": { "resourceType": "Patient", "id": "137202485", "meta": { "versionId": "12" } },
"request": { "method": "PUT", "url": "Patient/137202485/_history/12" },
"response": { "status": "200 OK", "etag": "W/\"12\"" }
},
{
"fullUrl": "https://hapi.fhir.org/baseR4/Patient/137202485",
"resource": { "resourceType": "Patient", "id": "137202485", "meta": { "versionId": "11" } },
"request": { "method": "PUT", "url": "Patient/137202485/_history/11" },
"response": { "status": "200 OK", "etag": "W/\"11\"" }
}
]
} The repeated fullUrl is allowed only in history Bundles (bdl-7). Deletes appear as DELETE requests with no resource, and conditional operations are recorded as plain updates and deletes. Filter with _since or _at and page with next links. Servers SHOULD accept a history Bundle POSTed as a transaction or batch, which enables replication, though original transaction boundaries are lost. For push-based change feeds, see our guide to real-time pipelines with FHIR Subscriptions.
How big can a FHIR Bundle be?
The R4 Bundle specification does not define a maximum size or entry count. Limits come from each server's configuration, gateway timeouts and memory. Design page sizes and transaction sizes around what your target server accepts, and move population-scale reads to Bulk Data export instead of paging through millions of searchset entries.
- Ask for less.
_elementsand_summaryshrink resources, but_summary=textcannot be combined with_include, and_elementsdoes not apply to included resources. Never write subsetted resources back. - Avoid wildcard and
:iterateincludes. The Search page warns they may slow responses significantly. - Keep each transaction to one clinical event. A failure then rolls back one visit, not a day of feed data.
- Send
Prefer: return=minimalon writes when locations are enough. Our entries still returned status, location and ETag. - Use Bulk Data for population reads.
$exportreturns NDJSON files instead of Bundles. Our Bulk FHIR export guide for Epic, Oracle Health and athenahealth covers it.
How to check whether an EHR supports transaction or batch
Fetch the server's CapabilityStatement from [base]/metadata and read rest.interaction. The system-level codes transaction and batch declare support. Then confirm with a harmless batch of GET requests, because declarations can be incomplete, and a sandbox statement does not guarantee what a customer's production server allows.
curl -s -H "Accept: application/fhir+json" "https://hapi.fhir.org/baseR4/metadata" \
| jq '.rest[] | {mode, interaction: [.interaction[]?.code]}' We ran that check against four public R4 endpoints. The results show why the CapabilityStatement is a starting point and not the final word:
| Server | Metadata URL | rest.interaction declared | What we observed |
|---|---|---|---|
| HAPI FHIR public test server | hapi.fhir.org/baseR4/metadata | transaction, history-system | Also processed our batch, which it does not declare |
| SMART Health IT R4 sandbox | r4.smarthealthit.org/metadata | history-system, transaction | Metadata only; no writes sent |
| Epic public R4 sandbox | fhir.epic.com/.../FHIR/R4/metadata | No system-level interaction element | Metadata only; per-resource entries not covered here |
| Oracle Health open R4 sandbox | fhir-open.cerner.com/r4/.../metadata | batch ("Implemented per the specification") | Metadata only; no writes sent |
The Epic sandbox CapabilityStatement and the Oracle Health open sandbox CapabilityStatement are both public. Neither settles what a specific Epic or Oracle Health customer's production server allows, so repeat the check on every production base URL. Servers without batch or transaction support SHOULD return HTTP 400, so try a one-entry batch before debugging a payload. More in our guide to reading an EHR's CapabilityStatement.
Writing into more than one EHR? Support for transaction, batch and conditional create differs by vendor and by customer site, and it decides how your write path has to be built. We build and run these integrations for product teams across Epic, Oracle Health, athenahealth, eClinicalWorks and NextGen. Talk to our team and we will map what each of your target servers actually accepts before you commit to a design.
Where teams get stuck with FHIR Bundles
Most Bundle bugs do not throw errors. They drop rows, double-count patients or leave half-written data, and they tend to surface only when real volumes hit a reconciliation report. These are the failure modes we see most when teams move from a sandbox to production traffic.
Stopping at page one
Sandbox patients often fit on one page, so a client that ignores next, or rebuilds URLs from _count and an offset, passes every test and then truncates in production.
Trusting Bundle.total and double-counting includes
Total can be missing or estimated, and it counts matches only. A pipeline that appends every entry duplicates Patients that repeat across pages. Split by search.mode and key includes on fullUrl.
Duplicate fullUrls in Bundles you build
Mapping code that mints a new urn:uuid: for every mention of the same patient creates two Patients, and reusing one UUID for two resources breaks bdl-7. Both can parse cleanly, which is why parsing vs clinical validation matters.
Assuming transactions work everywhere
HAPI rejected conditional references outright, and a server may declare batch without transaction. Plan a fallback early: resolve identifiers by search and send literal references, or sequence conditional creates with explicit cleanup. Finding this late means redesigning the write path.
Treating a batch HTTP 200 as success
The outer 200 only means the batch was processed. Parse every entry.response.status and retry or alert on anything outside 2xx.
Retrying a transaction that is not idempotent
Our first test transaction had ifNoneExist on the Patient and Encounter but not the Observation. The retry failed with HTTP 412 and HAPI-2840: Can not create resource duplicating existing resource: Observation/138554212. That came from this server's duplicate check; a server without one would have stored a second heart rate. Put ifNoneExist on every create you might retry.
POSTing to /Bundle instead of the base URL
The spec says a Bundle sent to the /Bundle endpoint is stored as-is and not processed as a batch, transaction or message. It looks like a successful write until someone searches for the data.
FHIR Bundle checklist before you ship
- Check
rest.interactionon each target server fortransactionandbatch, then confirm with a read-only batch. - Follow
nextlinks until none is returned; never build page URLs. - Split entries by
search.mode, deduplicate includes by fullUrl and logoutcomeentries. - Treat
Bundle.totalas optional and as a count of matches only. - Give each new resource a unique
urn:uuid:fullUrl and reference it from the other entries. - Put
ifNoneExiston every create that might be retried, keyed on a business identifier. - Use
ifMatchor a conditional update for existing resources. - Parse every
entry.response.statusin a batch-response and alert outside 2xx. - Validate document Bundles (Composition first, identifier, timestamp) and message Bundles (MessageHeader first) before sending.
- POST transactions and batches to the base URL, not to
/Bundle.
Bundles are where FHIR integrations quietly succeed or fail. Our healthcare interoperability team builds EHR integrations across Epic, Oracle Health, athenahealth, eClinicalWorks and NextGen, runs Mirth Connect interface engines in production, and maintains an open-source FHIR server that passes the ONC (g)(10) Inferno SMART App Launch test suite, 47 of 47. Our healthcare product engineering team builds the paging, retry and reconciliation layers around those connections. Talk to our team about the Bundle flows your product depends on.



