Why 'FHIR Compliant' Doesn't Mean Interoperable: Surviving Vendor-Specific Extensions Across Epic, Oracle, and athena
CTO & Co-Founder
CTO & Co-Founder at Nirmitee.io. Architects healthcare integrations across FHIR, SMART on FHIR, ABDM and NHCX, writing from production experience taking hospital software from sandbox to go-live.

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 (standard SMART 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).
Standard SMART Scopes, Practice-Level Access
athenahealth uses standard SMART on FHIR scopes, not a custom scope model. Its authorization overview accepts both SMART v1 scopes such as patient/Observation.read and SMART v2 scopes such as patient/Observation.rs; request the v2 form in new apps. system/ scopes are for 2-legged backend apps and patient/ and user/ scopes for 3-legged apps, and athenaOne APIs outside FHIR use athena/service/Athenanet.MDP.*. A common cause of access errors is the practice layer instead: per athenahealth's onboarding overview, an athenaOne customer must enable your app for its practice before your credentials can read that practice's data. Our athenahealth API integration guide walks through the scopes, token requests and practice enablement step by step.
When the product has to sell into athenaOne practices, our athenahealth integration services team maps each workflow to the certified FHIR APIs or the athenaOne APIs and handles the scope, validation and enablement steps.
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.
Seeing vendor extensions and CapabilityStatement gaps break your integration? Talk to our team for a fixed-scope architecture review of your multi-vendor FHIR layer, or explore our healthcare interoperability solutions.
Ready to scale?
Talk to our healthcare engineering team about building, integrating, and shipping faster.
Frequently Asked Questions
Why doesn't FHIR compliance guarantee interoperability?
How does Epic's FHIR implementation differ from other EHR vendors?
What are the quirks of Oracle Health's (Cerner's) FHIR API?
How is athenahealth's FHIR API different from Epic and Oracle Health?
How do you build a FHIR integration that works across Epic, Oracle Health, and athenahealth?
How does Nirmitee.io approach healthcare software development?


