A FHIR integration comes down to five decisions made in order: which FHIR version to target, how you will authenticate, which resources back your workflow, what the specific server actually supports, and how you will handle the gaps. In practice R4 is the version to build against, since it’s what Epic, Oracle Health and the US Core profiles are aligned on. Authentication is almost always SMART on FHIR over OAuth2, with the flow differing for patient-facing versus system-level apps. The step most teams skip is reading the server’s own CapabilityStatement, which is the machine-readable declaration of exactly which resources, search parameters and operations that deployment supports — and because no two implementations expose the same subset, an integration built against one vendor’s FHIR server routinely fails against another’s for reasons that are documented but never checked. Treat the CapabilityStatement as the first request you make, not the last thing you debug.
The Five Decisions Before You Write Code
Every real FHIR integration comes down to the same five decisions, made roughly in this order, and skipping ahead to the resource mapping before you’ve settled the earlier ones is where most avoidable rework comes from: version, authentication, resources, actual server support, and the gap-handling plan for wherever those don’t line up.
Which FHIR Version to Target
R4 is the practical answer in 2026, full stop. It’s what Epic and Oracle Health build against, and it’s what US Core — the profile set that gives FHIR its actual interoperability teeth in the U.S. — is aligned on. Older deployments running DSTU2 or STU3 still exist, and R5 is out there too, but if you’re starting fresh, R4 is where the ecosystem’s actual weight sits.
Here’s the thing worth internalizing before you go further: “supports FHIR” on a vendor’s marketing page tells you almost nothing. It doesn’t tell you the version, it doesn’t tell you which resources, and it doesn’t tell you which profiles. That vagueness is exactly why the next section matters more than most integration guides admit.
Authentication: SMART on FHIR and OAuth2
Almost every real FHIR integration authenticates through SMART on FHIR over OAuth2, and the flow splits into two patterns depending on what your app actually is. An EHR launch happens when a user opens your app from inside their existing EHR session — the EHR hands you a context handle you exchange for the actual patient and encounter. A standalone launch starts outside the EHR entirely, with the user picking a patient during authorization since there’s no session context to inherit.
Where teams actually get stuck: a build that authenticates cleanly against the public sandbox, then throws access errors the moment it points at a real client’s environment — because the specific scopes the app requests were never authorized in that client’s own tenant. Passing sandbox auth tells you your OAuth flow is implemented correctly. It doesn’t tell you your scope list has been approved anywhere real.
Start With the CapabilityStatement
This is the section that actually differentiates a team that’s shipped real FHIR integrations from one that’s read the spec once. Every conformant FHIR server exposes a single, standard endpoint — GET /metadata — and it returns a machine-readable CapabilityStatement declaring exactly which resource types, operations and search parameters that specific deployment actually supports.
Request it first, before you write a line of integration code. It answers the question no vendor marketing page will: does this specific server actually support the resource, the search parameter, the operation your workflow needs, or does it just claim general FHIR conformance while implementing a partial subset. Worth one honest caveat here too — CapabilityStatements are declarations, not test results, and servers do sometimes over-claim or under-claim what they actually support. Treat it as your starting map, not gospel, and verify against real behavior once you’re building.
Mapping Your Workflow to Resources
Once you know what a server actually exposes, map your real workflow onto FHIR’s resource model rather than the other way around:
- Patient — demographics and identity, the anchor for almost everything else
- Encounter — the visit or clinical event context
- Observation — labs, vitals, device readings, questionnaire results
- Condition — problem list and diagnosis history
- MedicationRequest — prescriptions and orders
- DocumentReference — notes, reports, and other unstructured clinical content
That’s the same core resource set that shows up in essentially every real integration regardless of which EHR vendor you’re pointed at, which is a good sign the design is transferable across the projects you’ll build next.
Search, Pagination and the Things That Break at Volume
What works cleanly against ten test patients in a sandbox doesn’t always hold up at real volume. Search-parameter support genuinely varies between servers — a parameter that’s technically optional in the spec might be missing entirely on a given deployment, which is exactly what the CapabilityStatement is supposed to have told you upfront. Pagination behavior differs too, and includes (_include, for pulling related resources in one request) aren’t universally supported the same way. If you’re building anything that needs to run against real patient volume rather than a handful of demo records, test the pagination and rate behavior explicitly — don’t assume it from the happy-path sandbox response.
Handling the Gaps: Extensions and Missing Data
No server exposes everything your workflow ideally wants, and how you handle that gap is a real design decision, not an afterthought. Sometimes the data exists but lives in a vendor-specific extension rather than a standard field. Sometimes it just isn’t there at all, and the honest options are to omit it, extend your own model to accommodate the gap, or — if the data genuinely matters and the gap is structural — fall back to HL7 v2 for that specific workflow instead of forcing FHIR to do a job it isn’t currently equipped for at that deployment. I go deeper on exactly when that fallback decision makes sense in a full breakdown of FHIR vs. HL7 v2.
Testing Against a Sandbox vs. Reality
Public sandboxes are genuinely useful for early development — they let you iterate without needing anyone’s permission first — but they’re built on synthetic data and configured generically, which means they can’t tell you how your integration will actually behave against a specific client’s real, messier configuration. Real adoption is broad enough now that this matters at scale: FHIR-based APIs went from 84% of hospitals implementing them in 2019 to 93% by 2024, and every certified EHR has been required to expose standardized FHIR APIs since January 2023. FHIR isn’t a niche capability anymore — it’s the baseline. Which means the variance you’ll hit isn’t whether a system supports FHIR at all, it’s exactly how that specific deployment implements it, and there’s no substitute for verifying real behavior against the target environment before you call an integration done.
Next Steps
The five decisions above are the same whether you’re building against Epic, Oracle Health, or a smaller EHR vendor — version, auth, resources, real server support, and a plan for the gaps. If you want the vendor-specific detail, I’ve written up how this plays out specifically against Cerner Millennium. And if you’re scoping a build and want a second set of eyes on the resource mapping before you commit to it, talk to our team — we’d rather catch a CapabilityStatement gap in week one than in your first production incident.
