Client and Context
Standfirst: Client details are anonymized under NDA. The Epic behavior described here was verified by our engineering team against Epic’s sandbox and API specifications catalog.
A US clinic network running multiple sites under one organization, with its own clinical operations and IT teams.
The sites did not share one record. Patient data that existed at one clinic had to reach another by hand — staff re-entering it, exporting it, or chasing it by phone — and the administrative load of that sat on people doing clinical work.
The Challenge
As the network grew, moving data between sites by hand stopped scaling, and that load grew with it. Each new clinic meant another set of access arrangements made by hand, another check of which clinical resources were actually reachable, another round of connection errors tracked as they surfaced, and a configuration that matched no other site. Bringing a clinic into the flow could take several days of coordination before any data moved.
The cost landed first on the staff doing the moving — clinical and administrative people spending their day on transcription and follow-up rather than on patients.
It reached the exam room, and that is the part worth stating plainly: the data a clinician needed was not there when a patient arrived from another site. It appeared once someone had finished moving it. A workflow that waits on a person completing a transfer carries that person’s queue into the clinic.
What was missing wasn’t a connection. It was a repeatable way to connect a new site to the EHR it runs — Epic or Oracle Health — without designing the integration again from scratch each time.
Why Epic?
Epic wasn’t a choice. A large share of US hospitals run it, and an integration adapts to the installed system rather than the other way round.
That single fact sets the FHIR version, the registration model, the scope syntax, the launch mechanics and the approval path for the entire build.
Our Approach
The same five stages described on our EHR Integration page:
Sandbox verification → access alignment with the organization → build → production readiness → go-live.
Sandbox Verification
A developer account on fhir.epic.com, a registered application, endpoints discovered at runtime rather than hardcoded, a named synthetic test patient, and a verified authenticated read. No connection to any production environment and no real patient data at any point.
Access Alignment
Epic access is not a switch we control. An Epic Community Member has to download the client record into their environment, and someone there needs the “Able to Purchase Apps?” security point to do it. Epic states that direct access to Epic software requires approval from both the community member and Epic. Because that approval sits outside the development team’s control, our approach is to open it in parallel with development rather than after it.
Build
Read paths first, against the resources the product actually needs, with error handling and search behavior verified per resource rather than assumed from the specification. Write paths are a separate capability with their own registration, scopes and organizational enablement, and are scoped deliberately rather than assumed from read access.
Production Readiness
Separate credentials, terminology mapping, patient matching, encounter association, audit logging, monitoring, reconciliation, and a defined correction path for wrong data.
Go-live
Go-live, on the organization’s timetable and after its security review.
A working sandbox integration proves the protocol works. It is not production readiness — stage 4 exists because those are different things.
Technical Solution — Epic Specifics
(Observed in Epic’s live vendor portal or verified against Epic’s sandbox.)
FHIR version. R4 (4.0.1). The sandbox advertises US Core 6.1.0 support.
Write APIs are catalog entries qualified by clinical context, not per-resource flags.
This is the single most consequential Epic-specific fact in the build, and it is where a team that has only read the FHIR specification goes wrong. “Epic supports Observation write” is not a statement anyone can act on. The catalog publishes Observation.Create (Vital Signs) (R4) — and, among the R4 entries reviewed, no Labs context at all.
The read side splits the same way.
Filtering the specifications catalog on Observation.Search returns 39 entries across FHIR versions, one per clinical context: Labs, Vital Signs, Social History, SDOH Assessments, Genomics and more. Registering Observation.Search (Labs) (R4) gives you no vital signs. An application that needs labs and vitals registers at least four entries — Read and Search for each. We derive that list from the product’s use cases before registering, rather than discovering it afterward.
Create and Update do not come in pairs.
Among the R4 entries reviewed, Observation.Create appears for Vital Signs, DICOM Image Characteristics, and Lines, Drains and Airways, while Observation.Update appears only for the last two. Being able to create a vital sign gives you no published way to change it. No .Patch or .Delete entries appeared among the R4 write entries reviewed.
Write paths identified per use case (R4 catalog entries):
Observation.Create (Vital Signs), QuestionnaireResponse.Create (Patient-Entered Questionnaires), DocumentReference.Create (Clinical Notes) plus two further document contexts, Condition.Create (Problems), AllergyIntolerance.Create (Patient Chart). No published R4 write entry was found for Immunization or Appointment among those reviewed.
Authorization: registration is the ceiling.
Epic derives resource scopes from the Incoming APIs on the application record, and the token response carries what the record permits intersected with what was requested. Verified against the sandbox — an application registered with exactly two Incoming APIs received user/Patient.read, user/Practitioner.read, fhirUser and openid, and a call to a resource never added to the record returned 403.
Token lifetimes.
Access token 3600 seconds; authorization code valid 300 seconds. We read the lifetime from the token response rather than hardcoding either.
Flow.
Authorization code with PKCE, S256 only — plain PKCE is not offered. The aud parameter is required on the authorization request, set to the FHIR base URL being called. Confidential clients use a public HTTPS JWK Set URL or a sandbox client secret; Epic no longer accepts uploaded public keys or X.509 certificates.
Endpoint discovery at runtime,
Endpoint discovery at runtime, from the capability statement and the SMART configuration document, never hardcoded beyond the sandbox base — an organization can override the FHIR server endpoint per client, which is why Epic documents sending an Epic-Client-ID header on the metadata call.
Management Scenarios
We designed and verified the key scenarios for managing EHR connections — connected systems and their state, resource access rights, authentication, audit trail and synchronization status — and built them into a working demo platform that shows how an administrator configures and monitors a connection.
The Obstacle: A Valid Token That Still Returned 403
This one is worth the space because it looks like an authentication failure and isn’t.
Authentication succeeded. The token was valid, correctly signed, and correctly scoped as far as anything in the response suggested. And the request to the FHIR resource we needed came back 403.
The instinct is to suspect the token — regenerate it, re-run the flow, check the signing key. All of that is wasted, because the token was never the problem. Access on Epic depends on which resources are permitted in the application’s own settings, not only on what the token carries. With Patient and Practitioner allowed but the resource we needed absent from the permitted list, the request fails on access while everything upstream of it looks correct.
Finding it took a few hours. The fix was to stop debugging the token and check the whole chain instead: application settings → granted permissions → the specific FHIR resource being requested. The break was at the first link.
Two behaviors made the diagnosis slower, and both are worth knowing before you meet them:
- Configuration changes don’t apply immediately, and an existing token never gains new rights. After granting the missing permission, the old token still failed; re-authentication was required to pick up the new scope. Epic documents that a new or changed application record can take up to an hour to sync with the sandbox. Change one thing at a time and wait out the window, or the result cannot be attributed to the change.
- HTTP 200 does not mean success. During that sync window, the authorization page returns HTTP 200 carrying an HTML page headed “OAuth2 Error” — not an OAuth error object, not a 4xx. An unsynced client and a client that does not exist produce byte-identical responses, so the response cannot distinguish “wait longer” from “your client ID is wrong” from “your redirect URI does not byte-match.” Checking the status code alone is not enough; the body has to be read.
Both behaviors are documented in the troubleshooting section of our own sandbox guide, because we hit them in our own verification work. Verifying the full permission chain before blaming the token is now a standing step in how we approach an Epic connection.
Compliance and Security
No real patient data in any vendor sandbox, at any point.
A shared sandbox has no consent, no BAA, and other developers on the same tenant. Epic’s terms permit Epic to delete identifiable data it finds there at its discretion.
Sandbox and production credentials held separately
Sandbox and production credentials held separately, with no shared configuration file, and no production callback URL ever registered on a sandbox application.
Logs carry identifiers and shapes, never content
Logs carry identifiers and shapes, never content — resource type, resource ID, status code, duration. No response bodies, patient names, clinical values, tokens or authorization codes. FHIR resource IDs are treated as sensitive in production: an ID is a direct handle on a clinical record for anyone holding a token, and log aggregators reach further than the application does.
Saved response bodies are treated as clinical content,
Saved response bodies are treated as clinical content, not retained casually in working directories or build artifacts.
Patient identity handled correctly at the data layer.
Patient identity handled correctly at the data layer. A FHIR resource ID is valid only for the server that issued it and is never stored as a permanent key for a person; business identifiers such as an MRN are matched on assigning system and value together.
BAA scope is established before the architecture is fixed,
BAA scope is established before the architecture is fixed, because whether one applies depends on the role each party plays and how data moves — an application that only renders data inside the organization’s session, one that stores data on its own infrastructure, and one operating under the patient’s own direction are treated differently.
Security review runs on the organization’s terms and timetable.
A sandbox result substitutes for none of it: a sandbox integration is not a HIPAA assessment, a penetration test or a clinical safety case.
Outcome
Data that arrives on its own (scenario).
Patient demographics, practitioner information and the selected clinical resources reach the receiving system through the FHIR API, inside the existing workflow, without anyone moving them by hand.
Transcription replaced by maintenance (scenario).
The manual transfer between sites stops being a daily task. What remains is maintaining the integrations themselves — a defined, bounded piece of work owned by one team rather than an administrative load spread across clinical staff.
A production path understood in advance (scenario).
Six to eight weeks, kickoff to production connection, covering sandbox verification, access configuration, security review and production preparation — with the approval steps that sit outside our control started early rather than discovered at the end.
403 on a valid token, and the hour-long sync window (verified).
Both were found in our own sandbox verification and are documented in our troubleshooting guide — which is why they are designed around rather than discovered on a customer’s timetable.
Planning an Epic integration?
We’ll walk you through the working platform and the implementation path for your use case.




