Vendor guide

# Connecting to Epic and Oracle Health FHIR APIs from Python

The two EHR vendors covering most US hospitals expose FHIR R4 APIs behind SMART on FHIR authorization. What versions they support, how the auth flows work and the Python code to call them.

R4 FHIR version both support

No R5 on either platform

OAuth 2.0 authorization

epic\_read\_patient.py

```
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class EpicReadPatient(Service):
    name = 'demo.ehr.epic-read-patient'

    def handle(self) -> 'None':

        # OAuth against Epic is configured on the
        # connection, tokens are managed for you
        client = self.fhir['FHIR.Epic']

        patient = client.get('Patient', 'eVgg3Buc6VmvyHU')

        family_name = patient.get_by_path('name.0.family')
        self.logger.info('Patient: %s', family_name)
```

Versions

## Which FHIR version do Epic and Oracle Health support?

Both expose FHIR R4 production endpoints and neither supports R5 - a position shared by the major EHR vendors, as the [FHIR versions](https://zato.io/docs/dev/healthcare/hl7/fhir/versions/index.html) page explains. Oracle Health (Cerner) deprecated its DSTU2 endpoints in December 2025, so R4 is the only version with continued support on these platforms.

Build on R4 and keep version assumptions out of the code - the connection definition holds the endpoint, the services hold the logic.

Authorization

## How does SMART on FHIR authorization work?

SMART on FHIR is OAuth 2.0 with healthcare conventions, and two flows matter. SMART App Launch is the user-facing flow - authorization code with PKCE - for apps that a clinician or patient launches, with the user's session defining what the app may see.

SMART Backend Services is the server-to-server flow - client credentials with a signed JWT assertion - for integration platforms that run without a user, which is the flow an integration layer uses. Your system is registered with the EHR, holds a key pair, and exchanges signed assertions for short-lived access tokens scoped to system-level resources.

| Flow | Who it is for | Grant type |
| --- | --- | --- |
| SMART App Launch | User-facing apps launched from the EHR or a portal | Authorization code with PKCE |
| SMART Backend Services | Server-to-server integrations without a user | Client credentials with signed JWT |

In practice

## How do I authenticate to Epic from Python?

Register your application - Epic's vendor programs and Oracle's developer console both issue a client ID for production access and take the public half of your key pair as a JWK set. Then define a bearer token definition whose authentication is a [private key JWT](https://zato.io/docs/dev/healthcare/hl7/fhir/security/index.html#smart-backend-services-with-a-private-key), holding the private key, the EHR's token URL and the system scopes, and assign it to an outgoing FHIR connection - Zato signs the client assertion of each token request, obtains and refreshes the tokens, and the services only ever refer to the connection by name. The same connection runs [bulk exports](https://zato.io/docs/dev/healthcare/hl7/fhir/bulk-export/index.html) of Epic's patient groups through the `$export` operation, on a schedule, with the NDJSON files delivered to SFTP, Kafka or a service of yours.

ehr\_observations.py

```
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class ReadVitals(Service):
    name = 'demo.ehr.read-vitals'

    def handle(self) -> 'None':

        # The connection points at the EHR's R4 endpoint,
        # the OAuth definition is attached to it in the Dashboard
        client = self.fhir['FHIR.Epic']

        # Vital signs for one patient, most recent first
        observations = client.resources('Observation').search(
            patient = 'eVgg3Buc6VmvyHU',
            category = 'vital-signs',
        ).sort('-date').limit(20)

        for observation in observations.fetch():
            code_text = observation.get_by_path('code.text')
            value = observation.get_by_path('valueQuantity.value')
            self.logger.info('%s: %s', code_text, value)
```

The same service works against Oracle Health - only the connection name changes, e.g. `self.fhir['FHIR.OracleHealth']`, with that connection pointing at the Oracle R4 endpoint and with its own OAuth definition.

Capabilities

## How do I read the CapabilityStatement first?

Every FHIR server describes itself at the metadata path - which resources it serves, which search parameters it supports, which auth endpoints to use. Reading it is the first request worth making against a new EHR endpoint, and [path access](https://zato.io/docs/dev/healthcare/hl7/fhir/path-access/index.html) pulls the details out of the deeply nested answer.

capability\_statement.py

```
# The CapabilityStatement describes the server
capability = client.execute(path='metadata', method='get')

# The FHIR version the server declares
fhir_version = capability['fhirVersion']

# The OAuth endpoints, from the SMART security extension
rest = capability.get_by_path(['rest', 0])

self.logger.info('Server is on FHIR %s', fhir_version)
```

Scope

## What does Zato do here and what does it not?

Zato is the FHIR client - it authenticates, reads and writes against the EHR's endpoints and integrates the results with the rest of your systems. Registering the application is done in Epic's and Oracle's own programs (Epic's vendor services and open.epic, Oracle's Ignite APIs), and Zato does not host a FHIR server of its own - the EHR is the server.

FAQ

## Frequently asked questions

Backend integrations use system scopes - e.g. system/Patient.read or, in SMART 2.0 notation, system/Patient.rs - listing each resource type the integration touches. The EHR's app registration controls which scopes are grantable.

Yes - both vendors run public R4 sandboxes with test patients, and services written against a sandbox connection move to production by changing the connection definition, not the code.

Neither vendor exposes R5 endpoints - US regulation and US Core profiles standardize on R4, and the US Core roadmap moves from R4 directly to a planned R6 without adopting R5, so R4 is what production integrations target.

Both are R4, so the resource structures match - differences concentrate in identifiers, extensions and supported search parameters. Keep those in configuration and per-connection lookups, and use path access with matchers instead of positional indexing.

Related

## Keep reading

[Versions FHIR versions in 2026 and 2027](https://zato.io/docs/dev/healthcare/hl7/fhir/versions/index.html) [TEFCA TEFCA and information blocking for integration teams](https://zato.io/integrations/healthcare/tefca-information-blocking-integration/) [CMS CMS-0057-F prior authorization and payer FHIR APIs](https://zato.io/integrations/healthcare/cms-0057-prior-authorization-fhir/) [Security FHIR security - Basic Auth, OAuth, TLS](https://zato.io/docs/dev/healthcare/hl7/fhir/security/index.html)

## Ready to connect to your EHR?

Get started with Zato and read your first Patient resource from Epic or Oracle Health in minutes.

[Get started ▶](https://zato.io/docs/admin/guide/install/index.html) [Read the docs](https://zato.io/docs/index.html)

Open source In Python
