
Every major EHR vendor will tell you their system is "FHIR compliant." They have ONC certification. They pass Inferno tests. They publish a CapabilityStatement. And yet, when you write a FHIR client that works against Epic's sandbox, it will fail against Oracle Health. When you build a patient data pipeline for athenahealth, it will return different data shapes than the same query against Epic.
This is not a bug -- it is a structural feature of how FHIR compliance works. FHIR is a framework, not a wire-level protocol. The specification defines the resource schemas, the REST interactions, and the search parameters, but it leaves enormous room for vendor interpretation. Extensions, custom search parameters, non-standard error handling, and proprietary authentication flows create a landscape where "FHIR compliant" is a necessary but insufficient condition for interoperability.
This guide maps the specific differences across the three largest EHR vendors in the US market, and provides the abstraction layer architecture pattern that lets you build once and connect to any vendor.
The Vendor FHIR Landscape in 2026

Before diving into specific differences, here is the high-level comparison matrix:
| Capability | Epic | Oracle Health (Cerner) | athenahealth |
|---|---|---|---|
| FHIR Version | R4 (primary) | R4 + DSTU2 (some resources) | R4 (with proprietary layer) |
| Auth Model | SMART on FHIR + Epic-specific | SMART on FHIR (standard) | OAuth 2.0 (custom scopes) |
| Custom Extensions | Extensive (100+ documented) | Moderate | Minimal (wraps proprietary API) |
| Search Params | Standard + custom Epic params | Standard (limited subset) | Standard (limited subset) |
| Bulk Data Export | Supported (with limitations) | Supported | Limited support |
| Write Operations | Limited (few writable resources) | Moderate | Through proprietary API preferred |
| Sandbox Quality | Excellent (realistic data) | Good | Basic |
| Documentation | Extensive (open.epic.com) | Good (fhir.cerner.com) | Moderate |
Epic: The Extension-Heavy Approach

Epic has the most mature FHIR implementation and the most extensive set of custom extensions. Their approach is to support the standard FHIR resources but enrich them with Epic-specific data through extensions.
Epic-Specific Search Parameters
Epic supports standard FHIR search parameters but adds proprietary ones that are often more useful than the standard alternatives. For example, Patient search in Epic supports own-name, own-prefix, and legal-name as search parameters -- none of which exist in the FHIR specification. If you build a patient lookup that depends on these, it will not work against any other vendor.
Proprietary Resource Extensions
Epic resources include extensions with URLs like http://open.epic.com/FHIR/StructureDefinition/.... Common examples include MyChart activation status on Patient, department identifiers on Encounter, and Epic-specific coding systems on Condition. These extensions carry clinically relevant data that may not be available through standard FHIR fields.
Authentication Quirks
While Epic supports SMART on FHIR, their implementation has specific requirements around client registration, redirect URIs, and scope handling that differ from the specification. Epic's App Orchard (now App Market) adds an additional distribution and approval layer that affects how your application authenticates across different Epic customer sites.
Oracle Health (Cerner): The DSTU2 Legacy
Oracle Health's FHIR implementation carries the legacy of being one of the earliest adopters. They were building FHIR APIs when DSTU2 was the current version, and some of that heritage remains.
Mixed FHIR Version Support
While Oracle Health's primary API is R4, some resources still return DSTU2 representations or have DSTU2-era behaviors. The DocumentReference resource, for example, may include fields structured according to DSTU2 conventions. This means your FHIR parser needs to handle version-specific field names and structures within the same API surface.
Limited Search Parameter Support
Oracle Health supports a narrower set of search parameters than the FHIR specification defines. A standard FHIR search like Observation?category=vital-signs&date=gt2025-01-01 might work, while the same query with additional chained parameters may return an error or be silently ignored. Always test your queries against the vendor's CapabilityStatement before assuming support.
Resource Coverage Gaps
Not every R4 resource is available through Oracle Health's API. Resources like ServiceRequest, NutritionOrder, and some clinical resources may be unavailable or return empty results. Check the CapabilityStatement and cross-reference with actual API behavior -- they do not always match.
athenahealth: The Proprietary Layer
athenahealth's approach is fundamentally different from Epic and Oracle Health. Their FHIR API is essentially a translation layer on top of their proprietary athenaNet API.
The athena API-First Architecture
athenahealth built their integration platform around a proprietary REST API (the athenaNet API) before FHIR became a regulatory requirement. Their FHIR endpoint translates between FHIR resource formats and the underlying athenaNet data model. This means some FHIR queries are efficient (mapped to native athenaNet calls) while others are slow or unsupported (requiring expensive translation).
Custom Scope Model
athenahealth's OAuth scopes do not always map cleanly to FHIR resource scopes. Standard SMART on FHIR scopes like patient/Observation.read work, but the granularity of access control is determined by the athenaNet permission model underneath. You may have FHIR scope for a resource but get access errors because the underlying athenaNet permission is not configured.
The CapabilityStatement Reality Check

FHIR's CapabilityStatement resource is supposed to be the machine-readable contract for what a server supports. In practice, it is an aspirational document that does not tell the full story.
What CapabilityStatements Miss
- Rate limits: No vendor publishes rate limits in the CapabilityStatement. Epic's varies by customer site. Oracle Health's is undocumented.
- Actual vs declared search parameters: A CapabilityStatement may list a search parameter as supported, but the implementation silently ignores it or returns errors for certain value combinations.
- Extension requirements: Some vendor APIs require proprietary extensions in write operations but do not declare this in the CapabilityStatement.
- Pagination behavior: Each vendor implements pagination differently. Bundle link structures, page sizes, and cursor behaviors vary.
The Hidden Requirements

Beyond the documented differences, there are hidden requirements that only emerge during actual API integration:
- Error response formats: FHIR specifies
OperationOutcomefor errors, but vendors include different levels of detail, different issue codes, and sometimes return non-FHIR error responses for infrastructure-level failures. - Date format strictness: Some vendors require full precision (datetime with timezone), while others accept partial dates. The same query works on one vendor and fails on another.
- Bundle entry ordering: When returning search results as Bundles, the entry ordering varies by vendor. If your code depends on a specific order, it will break across vendors.
- Reference resolution: How vendors handle contained resources, absolute references, and relative references differs. An
Observation.subjectreference might be a relative URL on Epic and a full URL on Oracle Health.
Building the Vendor-Agnostic Abstraction Layer

The solution is an abstraction layer that normalizes vendor-specific behaviors into a consistent internal model. Here is the architecture pattern, implemented in Python:
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any
import httpx
@dataclass
class NormalizedPatient:
"""Vendor-neutral patient representation."""
id: str
fhir_id: str
vendor: str
given_names: List[str]
family_name: str
birth_date: str
gender: str
identifiers: List[Dict[str, str]] = field(default_factory=list)
extensions: Dict[str, Any] = field(default_factory=dict)
class FHIRClient(ABC):
"""Abstract base for vendor-specific FHIR clients."""
def __init__(self, base_url: str, token: str):
self.base_url = base_url.rstrip('/')
self.client = httpx.Client(
base_url=self.base_url,
headers={"Authorization": f"Bearer {token}",
"Accept": "application/fhir+json"}
)
@abstractmethod
def get_patient(self, patient_id: str) -> NormalizedPatient:
pass
@abstractmethod
def search_patients(self, **kwargs) -> List[NormalizedPatient]:
pass
@abstractmethod
def get_observations(
self, patient_id: str, category: Optional[str] = None
) -> List[Dict]:
pass
class EpicFHIRClient(FHIRClient):
"""Epic-specific FHIR client with extension handling."""
def get_patient(self, patient_id: str) -> NormalizedPatient:
resp = self.client.get(f"/api/FHIR/R4/Patient/{patient_id}")
resp.raise_for_status()
data = resp.json()
# Extract Epic-specific extensions
extensions = {}
for ext in data.get("extension", []):
if "open.epic.com" in ext.get("url", ""):
key = ext["url"].split("/")[-1]
extensions[key] = ext.get("valueString",
ext.get("valueBoolean", ext.get("valueCode")))
name = data.get("name", [{}])[0]
return NormalizedPatient(
id=patient_id,
fhir_id=data["id"],
vendor="epic",
given_names=name.get("given", []),
family_name=name.get("family", ""),
birth_date=data.get("birthDate", ""),
gender=data.get("gender", ""),
identifiers=self._normalize_identifiers(data),
extensions=extensions
)
def search_patients(self, **kwargs) -> List[NormalizedPatient]:
# Map standard params to Epic-supported params
params = {}
if "family" in kwargs:
params["family"] = kwargs["family"]
if "given" in kwargs:
params["given"] = kwargs["given"]
# Epic-specific: supports own-name
if "legal_name" in kwargs:
params["legal-name"] = kwargs["legal_name"]
resp = self.client.get("/api/FHIR/R4/Patient", params=params)
resp.raise_for_status()
bundle = resp.json()
return [self._to_normalized(e["resource"])
for e in bundle.get("entry", [])]
class OracleHealthFHIRClient(FHIRClient):
"""Oracle Health (Cerner) FHIR client."""
def get_patient(self, patient_id: str) -> NormalizedPatient:
resp = self.client.get(f"/fhir/r4/Patient/{patient_id}")
resp.raise_for_status()
data = resp.json()
# Oracle Health may return DSTU2-style name structures
name = data.get("name", [{}])[0]
given = name.get("given", [])
# Handle DSTU2 legacy: given might be a string
if isinstance(given, str):
given = [given]
return NormalizedPatient(
id=patient_id,
fhir_id=data["id"],
vendor="oracle_health",
given_names=given,
family_name=name.get("family", ""),
birth_date=data.get("birthDate", ""),
gender=data.get("gender", ""),
identifiers=self._normalize_identifiers(data)
)
class FHIRClientFactory:
"""Factory to create the right client for each vendor."""
VENDORS = {
"epic": EpicFHIRClient,
"oracle_health": OracleHealthFHIRClient,
}
@classmethod
def create(cls, vendor: str, base_url: str,
token: str) -> FHIRClient:
client_class = cls.VENDORS.get(vendor)
if not client_class:
raise ValueError(f"Unsupported vendor: {vendor}")
return client_class(base_url, token)
# Usage
client = FHIRClientFactory.create(
vendor="epic",
base_url="https://fhir.epic.com/interconnect-fhir-oauth",
token="your-access-token"
)
patient = client.get_patient("erXuFYUfucBZaryVksYEcMg3")
Testing Across Vendors

A robust multi-vendor testing strategy has three layers:
Layer 1: Unit Tests with Vendor-Specific Fixtures
Capture actual API responses from each vendor sandbox and use them as test fixtures. This lets you verify your normalization logic without hitting live APIs. Store fixtures as JSON files organized by vendor and resource type. Avoiding common integration mistakes starts with comprehensive fixture-based testing.
Layer 2: Integration Tests Against Sandboxes
Each vendor provides sandbox environments. Run your full integration test suite against each sandbox regularly (at least weekly). Track API behavior changes over time -- vendors update their implementations without notice.
Layer 3: Production Behavioral Monitoring
In production, log every FHIR API response structure (not the PHI content). Track field presence, extension types, and error patterns per vendor instance. When a vendor updates their API, your monitoring will detect the behavior change before it causes failures. This aligns with the observability framework for healthcare AI we have previously described.
Practical Recommendations
- Never assume FHIR compliance means compatibility. Test every query against every vendor you need to support. A passing Inferno test does not mean your specific workflow will work.
- Read CapabilityStatements programmatically. At startup, fetch the vendor's CapabilityStatement and validate that the resources and search parameters your application needs are actually declared.
- Build extension-tolerant parsers. Your FHIR parser should preserve unknown extensions rather than discarding them. Epic extensions that are irrelevant today may become critical tomorrow.
- Use integration middleware. For high-volume, multi-vendor integrations, Mirth Connect or similar integration engines can handle vendor-specific translation at the infrastructure level.
- Version your vendor adapters independently. Epic may update their API in March, Oracle Health in June. Each vendor adapter should have its own version and release cycle.
- Join vendor developer communities. Epic's developer forums, Oracle Health's developer community, and athenahealth's developer portal are where breaking changes are first announced.
Frequently Asked Questions
Is one EHR vendor more FHIR-compliant than others?
Epic has the most complete FHIR implementation in terms of resource coverage and search parameter support. Oracle Health has the longest history with FHIR (started with DSTU2). athenahealth has the most standard-compliant auth flow but the narrowest resource coverage. "Most compliant" depends on which resources and operations your application needs.
Will FHIR R6 reduce vendor differences?
Normative resources in R6 will narrow the specification interpretation gaps, but vendor-specific extensions and authentication models will remain. The biggest improvement will be in consistency of resource structures for normative resources.
Should I use a FHIR aggregation platform instead of building adapters?
Platforms like Flexpa, Health Gorilla, and Particle Health aggregate across vendors and provide a normalized API. They add cost but remove the maintenance burden of vendor-specific adapters. For startups connecting to fewer than 10 health systems, building your own adapter layer is feasible. Above 10, consider an aggregator.
How do I handle vendor-specific extensions in my data model?
Store vendor-specific extensions in a generic extension map (JSON column in your database) rather than creating vendor-specific schema fields. This keeps your data model vendor-neutral while preserving all available data. Query vendor extensions at the application layer when needed.
What happens when a vendor changes their FHIR API?
Vendors typically announce breaking changes 60-90 days in advance through developer portals. Non-breaking changes (new extensions, additional resources) may appear without notice. This is why production behavioral monitoring is essential -- it detects changes that announcements miss.
Can I use the same SMART on FHIR app across all vendors?
In theory, yes -- that is the point of SMART on FHIR. In practice, each vendor's App Market requires separate registration, review, and approval. Your application code should be portable, but the deployment process is vendor-specific. Budget 2-6 months for each vendor's app marketplace approval process.



