Building Your Own Epic FHIR Integration: A Complete Guide
Technology Blogs

Building Your Own Epic FHIR Integration: A Complete Guide

Ronak J
Associate Software Engineer
Table of Content

Electronic Health Records (EHR) integration is critical for modern healthcare applications. Epic Systems, one of the largest EHR vendors, provides a FHIR (Fast Healthcare Interoperability Resources) API that allows developers to access and exchange patient data securely. This guide will walk you through the fundamentals of Epic FHIR integration, covering the basics of authentication and making your first API requests.

Whether you’re building a patient portal, clinical decision support system, or healthcare analytics platform, this guide will help you understand the core concepts and get started with Epic FHIR integration.

I. Understanding Epic FHIR API

A. What is FHIR?

FHIR (Fast Healthcare Interoperability Resources) is an HL7 standard for exchanging healthcare information electronically. It uses modern web technologies like REST APIs and JSON, making it easier to work with than older standards like HL7 v2.

B. Epic’s FHIR Implementation

Epic implements FHIR R4 (Release 4) and provides two environments:

C. Common FHIR Resources

You’ll typically work with these resources:

  • Patient: Demographics and contact information
  • Observation: Vital signs, lab results, clinical observations
  • Condition: Diagnoses and health conditions
  • MedicationRequest: Prescriptions and medication orders
  • Appointment: Scheduled patient visits
  • Encounter: Patient visits and interactions

II. Authentication: OAuth 2.0 Backend Services

Epic uses the OAuth 2.0 Backend Services flow with JWT (JSON Web Tokens) for authentication. This is more secure than traditional API keys.

A. Prerequisites

Before you start, you’ll need:

  1. Client ID: Obtained from Epic’s registration process
  2. Private Key: RSA private key (typically 2048-bit or 4096-bit)
  3. Public Key: Uploaded to Epic during app registration
  4. Base URL: Your Epic FHIR endpoint

B. Authentication Flow

The authentication process involves:

  1. Creating a signed JWT
  2. Exchanging the JWT for an access token
  3. Using the access token in API requests
  4. Refreshing tokens before expiration

C. Implementation: Creating the JWT

import jwt

import time

import uuid

from datetime import datetime, timedelta

def create_jwt(client_id, private_key_path, token_url):

    """

    Create a signed JWT for Epic authentication.

    Args:

        client_id: Your Epic client ID

        private_key_path: Path to your RSA private key

        token_url: Epic's token endpoint

    Returns:

        Signed JWT string

    """

    # Read private key

    with open(private_key_path, 'r') as key_file:

        private_key = key_file.read()  

    # JWT claims

    now = int(time.time())

    claims = {

        'iss': client_id,  # Issuer: your client ID

        'sub': client_id,  # Subject: also your client ID

        'aud': token_url,  # Audience: Epic's token endpoint

        'jti': str(uuid.uuid4()),  # Unique identifier for this JWT

        'exp': now + 300,  # Expiration: 5 minutes from now

        'iat': now,  # Issued at: current time

    }   

    # Sign with RS384 algorithm (required by Epic)

    signed_jwt = jwt.encode(claims, private_key, algorithm='RS384')

    return signed_jwt

D. Implementation: Exchanging JWT for Access Token

import aiohttp

async def get_access_token(client_id, private_key_path, token_url):

    """

    Exchange JWT for an access token.   

    Args:

        client_id: Your Epic client ID

        private_key_path: Path to your RSA private key

        token_url: Epic's token endpoint    

    Returns:

        Access token string

    """

    # Create JWT

    jwt_token = create_jwt(client_id, private_key_path, token_url)   

    # Prepare request

    data = {

        'grant_type': 'client_credentials',

        'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',

        'client_assertion': jwt_token,

    }    

    # Exchange JWT for access token

    async with aiohttp.ClientSession() as session:

        async with session.post(token_url, data=data) as response:

            if response.status != 200:

                error_text = await response.text()

                raise Exception(f"Authentication failed: {error_text}")          

            token_data = await response.json()

            return token_data['access_token']

E. Best Practices for Token Management

  1. Cache tokens: Access tokens are valid for 1 hour (typically). Cache them to avoid unnecessary authentication requests.
  2. Refresh proactively: Refresh tokens before they expire (e.g., at 55 minutes).
  3. Handle 401 errors: If you get a 401 (Unauthorized), invalidate the cached token and get a new one.
from datetime import datetime, timedelta

class TokenCache:

    """Simple token cache with expiration."""    

    def __init__(self):

        self.token = None

        self.expires_at = None    

    def get_token(self):

        """Get cached token if still valid."""

        if self.token and self.expires_at > datetime.now():

            return self.token

        return None    

    def set_token(self, token, expires_in_seconds=3300):

        """Cache token with expiration (default 55 minutes)."""

        self.token = token

        self.expires_at = datetime.now() + timedelta(seconds=expires_in_seconds)    

    def invalidate(self):

        """Invalidate cached token."""

        self.token = None

        self.expires_at = Non

III. Making Your First FHIR Request

Once authenticated, you can make FHIR requests using the access token.

A. Basic GET Request

async def get_patient(patient_id, access_token, base_url):

    """

    Retrieve a patient by ID.    

    Args:

        patient_id: Epic patient ID

        access_token: Valid access token

        base_url: Epic FHIR base URL    

    Returns:

        Patient resource as dictionary

    """

    url = f"{base_url}/Patient/{patient_id}"    

    headers = {

        'Authorization': f'Bearer {access_token}',

        'Accept': 'application/fhir+json',

    }    

    async with aiohttp.ClientSession() as session:

        async with session.get(url, headers=headers) as response:

            if response.status == 200:

                return await response.json()

            else:

                error_text = await response.text()

                raise Exception(f"Request failed: HTTP {response.status} - {error_text}")

B. Search Requests

FHIR supports powerful search capabilities:

async def search_observations(patient_id, category, access_token, base_url):

    """

    Search for observations by patient and category.

    Args:

        patient_id: Epic patient ID

        category: Observation category (e.g., 'vital-signs', 'laboratory')

        access_token: Valid access token

        base_url: Epic FHIR base URL

    Returns:

        Bundle of observations

    """

    url = f"{base_url}/Observation"

    params = {

        'patient': patient_id,

        'category': category,

        '_count': 20,  # Results per page

        '_sort': '-date',  # Sort by date descending

    }

    headers = {

        'Authorization': f'Bearer {access_token}',

        'Accept': 'application/fhir+json',

    }

    async with aiohttp.ClientSession() as session:

        async with session.get(url, headers=headers, params=params) as response:

            if response.status == 200:

                return await response.json()

            else:

                error_text = await response.text()

                raise Exception(f"Search failed: {error_text}")

C. Common Search Parameters

ParameterDescriptionExample
_countResults per page_count=50
_sortSort results_sort=-date (descending)
dateFilter by datedate=ge2024-01-01 (greater than or equal)
_includeInclude related resources_include=Observation:patient
_revincludeInclude resources that reference this_revinclude=Observation:subject
coma

Conclusion

This guide covered the fundamentals of Epic FHIR integration:

  1. Understanding Epic FHIR API: The basics of FHIR resources and Epic’s implementation
  2. Authentication: How to implement OAuth 2.0 Backend Services with JWT
  3. Making FHIR Requests: Performing GET requests and searches with proper parameters

These core concepts provide the foundation for building Epic FHIR integrations. As you develop your application, you’ll want to consider additional patterns like rate limiting, retry logic, connection pooling, and pagination handling for production deployments.

A. Additional Resources

B. Getting Started

Epic provides a sandbox environment for testing:

Start with the sandbox, experiment with the basic patterns shown in this guide, and expand your implementation as needed!

Ronak J

Ronak J

Associate Software Engineer

Ronak is a full-stack developer with around 2+ years of experience. He is an expert in building python Integrated web applications, creating REST API’s with well-designed, well-structured, and efficient code. He is eager to learn new programming languages and technologies and looking for new ways to optimize the development process.

Share This Blog

Read More Similar Blogs

Let’s Transform
Healthcare,
Together.

Partner with us to design, build, and scale digital solutions that drive better outcomes.

Location

5900 Balcones Dr, Ste 100-7286, Austin, TX 78731, United States

Contact form