Fetching FHIR R4 Resources for OpenMed
FHIR R4 is the modern EHR API: a RESTful, JSON-or-XML interface over resources
like Patient, Encounter, Condition, Observation, DiagnosticReport,
and DocumentReference. The unstructured clinical text you want for NLP lives
in DocumentReference.content.attachment and
DiagnosticReport.presentedForm — usually base64-encoded PDF, RTF, or
plain text. This skill pulls those resources, pages through results, decodes the
attachments, and hands the narrative to OpenMed.
When to use
- You have FHIR R4 access to an EHR (Epic, Oracle Health/Cerner, HAPI, Medplum,
Azure/Google/AWS HealthLake) and want note text for de-id and NER.
- You need to page a large search result set safely (
Bundle.link[next]).
- You want to pull a patient's documents/reports and rejoin NLP output by
patient and encounter.
FHIR REST in one minute
Search is GET [base]/[Type]?param=value. Results come back as a
searchset Bundle; the next page is the URL in Bundle.link where
relation == "next". Use _count to size pages, _revinclude to pull related
resources in one round trip, and _since/_lastUpdated for incremental sync.
GET /Patient?identifier=http://hospital.org/mrn|12345
GET /DocumentReference?patient=Patient/abc&category=clinical-note&_count=50
GET /DiagnosticReport?patient=Patient/abc&_revinclude=Observation:related
Quick start
Page a search, decode attachments, hand narrative to OpenMed:
import base64
import requests
import openmed
BASE = "https://fhir.example.org/r4"
HEADERS = {"Accept": "application/fhir+json", "Authorization": "Bearer <token>"}
def iter_bundle(url, params=None):
"""Yield resources across all pages following Bundle.link[next]."""
while url:
bundle = requests.get(url, params=params, headers=HEADERS, timeout=30).json()
for entry in bundle.get("entry", []):
yield entry.get("resource", {})
params = None # next links are fully-qualified
url = next(
(l["url"] for l in bundle.get("link", []) if l.get("relation") == "next"),
None,
)
def attachment_text(att):
"""Decode a FHIR Attachment to text (handles base64 and inline text/plain)."""
if att.get("data"):
raw = base64.b64decode(att["data"])
if att.get("contentType", "").startswith("text/"):
return raw.decode("utf-8", "replace")
return "" # PDF/RTF: route to OpenMed multimodal/OCR intake instead
return ""
# Pull a patient's clinical notes and analyze each.
for doc in iter_bundle(f"{BASE}/DocumentReference",
{"patient": "Patient/abc",
"category": "clinical-note", "_count": 50}):
for content in doc.get("content", []):
text = attachment_text(content.get("attachment", {}))
if not text.strip():
continue
deid = openmed.deidentify(text, method="replace", policy="hipaa_safe_harbor")
result = openmed.analyze_text(deid.text, output_format="dict")
patient_ref = doc.get("subject", {}).get("reference") # rejoin key
Workflow
- Authenticate. Most production FHIR endpoints use SMART-on-FHIR OAuth2
(client-credentials for backend services). Scope to the minimum
(
system/DocumentReference.read, system/DiagnosticReport.read).
- Search narrowly. Filter by
patient, category, type (LOINC),
date, and _count. Prefer server-side filtering over client-side.
- Page via
Bundle.link[next] until exhausted. Never assume one page.
- Extract narrative:
DocumentReference.content.attachment and
DiagnosticReport.presentedForm. Decode base64; for PDF/RTF/scanned
content, route bytes to OpenMed's document intake (multimodal/ocr)
rather than decoding as UTF-8.
- De-identify → analyze each narrative with OpenMed.
- Rejoin results to
subject.reference (patient) and context.encounter
so downstream consumers can group by patient/encounter — storing hashed,
not raw, identifiers.
Hand-off to / from OpenMed
To OpenMed (client-side): decoded narrative → openmed.deidentify →
openmed.analyze_text. Carry subject.reference as the rejoin key.
Server-side $de-identify: openmed.interop.fhir_operations implements
the FHIR $de-identify operation logic over the OpenMed privacy pipeline:
de_identify_resource(resource, policy=..., method=...)
de_identify_bundle(bundle, policy=..., method=...)
de_identify(parameters) — accepts/returns a Parameters envelope and
reports modified element paths as an OperationOutcome.
It de-identifies free-text strings, identifier values, and text.div
narrative while never altering codes, references, systems, or temporal
values. Use this to de-identify a whole fetched Bundle before storage:
from openmed.interop.fhir_operations import de_identify_bundle
safe_bundle = de_identify_bundle(bundle, policy="hipaa_safe_harbor",
method="replace")
Onward: re-export structured findings with
openmed.clinical.exporters.fhir (to_bundle, to_operation_outcome).
Edge cases & gotchas
- Attachments are often base64.
attachment.data is base64; large files use
attachment.url (a separate Binary fetch) instead. Handle both.
- Non-text content types.
application/pdf, text/rtf, scanned TIFF — do
not utf-8 decode these; send bytes to OpenMed multimodal/OCR intake.
- Pagination loops. Some servers emit cyclic or stale
next links; cap page
count and dedupe by resource id.
_revinclude vs _include. _include pulls referenced resources;
_revinclude pulls resources that reference yours. Mixing them changes
Bundle entry search.mode (match vs include) — filter on it.
- Versioning & profiles. Confirm the server is R4 (
/metadata
CapabilityStatement) and US Core-conformant; field cardinality differs across
FHIR versions.
- Throttling. Respect
429/Retry-After; batch with _count and back off.
- PHI everywhere. A FHIR resource is PHI by definition — never log raw
resources; de-identify before persistence or analytics.
Standards & references
1---2name: fetching-fhir-resources3description: Fetches and pages FHIR R4 resources (Patient, DocumentReference, DiagnosticReport, Observation, Condition) from a FHIR REST server, decodes base64 attachments, and extracts clinical narrative for OpenMed. Use before OpenMed processing when pulling charts from an EHR FHIR API (Epic, Cerner/Oracle, HAPI, or any US Core server) and you need the note text de-identified and analyzed, then results rejoined by patient. Hand narrative to openmed.deidentify and openmed.analyze_text; openmed.interop.fhir_operations implements a $de-identify operation over Bundles. Trigger keywords: FHIR, R4, US Core, DocumentReference, DiagnosticReport, Bundle, _revinclude, presentedForm, base64, EHR API.4license: Apache-2.05---67# Fetching FHIR R4 Resources for OpenMed89FHIR R4 is the modern EHR API: a RESTful, JSON-or-XML interface over resources10like `Patient`, `Encounter`, `Condition`, `Observation`, `DiagnosticReport`,11and `DocumentReference`. The unstructured clinical text you want for NLP lives12in **`DocumentReference.content.attachment`** and13**`DiagnosticReport.presentedForm`** — usually **base64-encoded** PDF, RTF, or14plain text. This skill pulls those resources, pages through results, decodes the15attachments, and hands the narrative to OpenMed.1617## When to use1819- You have FHIR R4 access to an EHR (Epic, Oracle Health/Cerner, HAPI, Medplum,20 Azure/Google/AWS HealthLake) and want note text for de-id and NER.21- You need to page a large search result set safely (`Bundle.link[next]`).22- You want to pull a patient's documents/reports and rejoin NLP output by23 patient and encounter.2425## FHIR REST in one minute2627Search is `GET [base]/[Type]?param=value`. Results come back as a28**searchset `Bundle`**; the next page is the URL in `Bundle.link` where29`relation == "next"`. Use `_count` to size pages, `_revinclude` to pull related30resources in one round trip, and `_since`/`_lastUpdated` for incremental sync.3132```33GET /Patient?identifier=http://hospital.org/mrn|1234534GET /DocumentReference?patient=Patient/abc&category=clinical-note&_count=5035GET /DiagnosticReport?patient=Patient/abc&_revinclude=Observation:related36```3738## Quick start3940Page a search, decode attachments, hand narrative to OpenMed:4142```python43import base6444import requests45import openmed4647BASE = "https://fhir.example.org/r4"48HEADERS = {"Accept": "application/fhir+json", "Authorization": "Bearer <token>"}4950def iter_bundle(url, params=None):51 """Yield resources across all pages following Bundle.link[next]."""52 while url:53 bundle = requests.get(url, params=params, headers=HEADERS, timeout=30).json()54 for entry in bundle.get("entry", []):55 yield entry.get("resource", {})56 params = None # next links are fully-qualified57 url = next(58 (l["url"] for l in bundle.get("link", []) if l.get("relation") == "next"),59 None,60 )6162def attachment_text(att):63 """Decode a FHIR Attachment to text (handles base64 and inline text/plain)."""64 if att.get("data"):65 raw = base64.b64decode(att["data"])66 if att.get("contentType", "").startswith("text/"):67 return raw.decode("utf-8", "replace")68 return "" # PDF/RTF: route to OpenMed multimodal/OCR intake instead69 return ""7071# Pull a patient's clinical notes and analyze each.72for doc in iter_bundle(f"{BASE}/DocumentReference",73 {"patient": "Patient/abc",74 "category": "clinical-note", "_count": 50}):75 for content in doc.get("content", []):76 text = attachment_text(content.get("attachment", {}))77 if not text.strip():78 continue79 deid = openmed.deidentify(text, method="replace", policy="hipaa_safe_harbor")80 result = openmed.analyze_text(deid.text, output_format="dict")81 patient_ref = doc.get("subject", {}).get("reference") # rejoin key82```8384## Workflow85861. **Authenticate.** Most production FHIR endpoints use SMART-on-FHIR OAuth287 (client-credentials for backend services). Scope to the minimum88 (`system/DocumentReference.read`, `system/DiagnosticReport.read`).892. **Search narrowly.** Filter by `patient`, `category`, `type` (LOINC),90 `date`, and `_count`. Prefer server-side filtering over client-side.913. **Page** via `Bundle.link[next]` until exhausted. Never assume one page.924. **Extract narrative:** `DocumentReference.content.attachment` and93 `DiagnosticReport.presentedForm`. Decode base64; for PDF/RTF/scanned94 content, route bytes to OpenMed's document intake (`multimodal`/`ocr`)95 rather than decoding as UTF-8.965. **De-identify → analyze** each narrative with OpenMed.976. **Rejoin** results to `subject.reference` (patient) and `context.encounter`98 so downstream consumers can group by patient/encounter — storing hashed,99 not raw, identifiers.100101## Hand-off to / from OpenMed102103- **To OpenMed (client-side):** decoded narrative → `openmed.deidentify` →104 `openmed.analyze_text`. Carry `subject.reference` as the rejoin key.105- **Server-side `$de-identify`:** `openmed.interop.fhir_operations` implements106 the FHIR `$de-identify` *operation logic* over the OpenMed privacy pipeline:107 - `de_identify_resource(resource, policy=..., method=...)`108 - `de_identify_bundle(bundle, policy=..., method=...)`109 - `de_identify(parameters)` — accepts/returns a `Parameters` envelope and110 reports modified element paths as an `OperationOutcome`.111 It de-identifies free-text strings, identifier values, and `text.div`112 narrative while never altering codes, references, systems, or temporal113 values. Use this to de-identify a whole fetched Bundle before storage:114115 ```python116 from openmed.interop.fhir_operations import de_identify_bundle117 safe_bundle = de_identify_bundle(bundle, policy="hipaa_safe_harbor",118 method="replace")119 ```120- **Onward:** re-export structured findings with121 `openmed.clinical.exporters.fhir` (`to_bundle`, `to_operation_outcome`).122123## Edge cases & gotchas124125- **Attachments are often base64.** `attachment.data` is base64; large files use126 `attachment.url` (a separate Binary fetch) instead. Handle both.127- **Non-text content types.** `application/pdf`, `text/rtf`, scanned TIFF — do128 not `utf-8` decode these; send bytes to OpenMed multimodal/OCR intake.129- **Pagination loops.** Some servers emit cyclic or stale `next` links; cap page130 count and dedupe by resource `id`.131- **`_revinclude` vs `_include`.** `_include` pulls referenced resources;132 `_revinclude` pulls resources that *reference* yours. Mixing them changes133 Bundle entry `search.mode` (`match` vs `include`) — filter on it.134- **Versioning & profiles.** Confirm the server is R4 (`/metadata`135 CapabilityStatement) and US Core-conformant; field cardinality differs across136 FHIR versions.137- **Throttling.** Respect `429`/`Retry-After`; batch with `_count` and back off.138- **PHI everywhere.** A FHIR resource is PHI by definition — never log raw139 resources; de-identify before persistence or analytics.140141## Standards & references142143- FHIR R4 specification: https://hl7.org/fhir/R4/144- FHIR RESTful API & search: https://hl7.org/fhir/R4/http.html and145 https://hl7.org/fhir/R4/search.html146- US Core Implementation Guide: https://hl7.org/fhir/us/core/147- DocumentReference: https://hl7.org/fhir/R4/documentreference.html148- DiagnosticReport (`presentedForm`): https://hl7.org/fhir/R4/diagnosticreport.html149- SMART on FHIR (backend services auth): https://hl7.org/fhir/smart-app-launch/