SMART on FHIR App Development Tutorial | Guide

Table of Contents

Share this article
SMART on FHIR App Development Tutorial

SMART on FHIR App Development Tutorial: Build EHR-Integrated Healthcare Apps (2026)


Key Takeaways

  • SMART on FHIR (Substitutable Medical Applications, Reusable Technologies) is the standard framework for building third-party applications that launch from within EHR systems and access clinical data through FHIR APIs. It is supported by Epic, Oracle Health (Cerner), Athenahealth, and virtually every certified EHR in the United States.

  • SMART on FHIR combines HL7 FHIR R4 for data access with OAuth 2.0 for authorization, enabling apps to securely access patient data within the clinician’s existing workflow — no separate login, no context switching, no manual patient lookup.

  • A single SMART on FHIR app can work across multiple EHR platforms. Build once, deploy to Epic, Oracle Health, Athenahealth, and any other SMART-enabled EHR. This cross-platform portability is the core value proposition for health tech developers.

  • The 21st Century Cures Act and ONC certification requirements mandate SMART on FHIR support for certified EHRs, making this framework the regulatory standard — not just a technical preference.

  • Taction Software’s SMART on FHIR implementations consistently show that apps built on this framework achieve 60–70% faster deployment across multiple EHR platforms compared to building custom integrations for each EHR separately.

What Is SMART on FHIR

SMART on FHIR is an open-standards-based framework that allows developers to build applications that integrate with any EHR system supporting the SMART specification. It was originally developed at Boston Children’s Hospital and Harvard Medical School, and is now maintained by HL7 International as a core part of the FHIR ecosystem.

The framework solves a fundamental problem in healthcare IT: before SMART on FHIR, building an application that worked inside an EHR required proprietary integration with each vendor. An app built for Epic could not run in Cerner without a complete rebuild. SMART on FHIR eliminates this by providing a standardized launch protocol, authorization mechanism, and data access layer.

What SMART provides:

  • A standardized way to launch apps from within an EHR (launch framework)
  • A standardized way to authenticate and authorize data access (OAuth 2.0 profiles)
  • A standardized data format and query language (FHIR R4)
  • Context sharing between the EHR and the app (which patient, which encounter, which user)

What SMART does NOT provide:

  • A UI framework (you build your own interface)
  • A hosting platform (you host your own application)
  • A database (you use FHIR APIs to access EHR data at runtime)
  • Write access by default (write permissions require additional authorization)

How SMART on FHIR Works

The SMART on FHIR workflow has three phases:

Phase 1: Discovery

Your app discovers the EHR’s FHIR server capabilities by fetching the server’s metadata:

GET {fhir-base-url}/.well-known/smart-configuration

This returns a JSON document containing:

  • authorization_endpoint — where to send the user for authentication
  • token_endpoint — where to exchange authorization codes for access tokens
  • scopes_supported — which FHIR scopes the server supports
  • capabilities — which SMART features are supported (launch-ehr, launch-standalone, etc.)

Alternatively, you can fetch the FHIR CapabilityStatement:

GET {fhir-base-url}/metadata

This returns the server’s FHIR capabilities including supported resources, search parameters, and security extensions containing the OAuth endpoints.

Why discovery matters: Different EHR systems have different OAuth endpoints, different supported scopes, and different capabilities. Your app must dynamically discover these rather than hardcoding them. This is what makes your app portable across EHR platforms.

Phase 2: Authorization

Your app requests authorization to access data on behalf of the user:

  1. Redirect the user’s browser to the authorization_endpoint with your requested scopes
  2. The EHR’s authorization server authenticates the user (clinician or patient)
  3. The user consents to the requested data access
  4. The authorization server redirects back to your app with an authorization code
  5. Your app exchanges the authorization code for an access token at the token_endpoint

Phase 3: Data Access

Your app uses the access token to make FHIR API calls:

GET {fhir-base-url}/Patient/{id} Authorization: Bearer {access-token}

The access token is scoped to specific FHIR resources and specific patients based on the launch context and requested scopes. Your app cannot access data outside the authorized scope.


SMART Launch Framework

The SMART launch framework defines how your app receives context from the EHR — which patient is selected, which encounter is active, which user is logged in.

Launch Context Parameters

When an EHR launches your app, it provides context through the OAuth token response:

Context ParameterDescriptionExample Value
patientFHIR Patient resource ID“12345”
encounterFHIR Encounter resource ID“67890”
fhirUserFHIR resource URL of the logged-in user“Practitioner/11111”
need_patient_bannerWhether EHR is already showing patient infotrue/false
smart_style_urlURL to EHR’s style guide for visual consistencyURL string
tenantTenant identifier for multi-tenant systems“hospital-a”

patient is the most critical parameter. It tells your app which patient’s data to access. In an EHR launch, this is automatically set to the patient whose chart the clinician has open. In a standalone launch, the user selects a patient during the authorization flow.

Launch URL Registration

When registering your app with an EHR, you provide:

  • Launch URL — the URL the EHR calls when launching your app
  • Redirect URL — where the authorization server sends the user after authentication
  • Scopes — which FHIR data your app needs access to

The EHR appends a launch parameter and an iss (issuer) parameter to your Launch URL:

https://your-app.com/launch?iss=https://fhir.hospital.com/r4&launch=abc123

Your app uses the iss to discover the authorization endpoint and the launch to correlate the session with the EHR context.


EHR Launch vs Standalone Launch

EHR Launch

How it works: A clinician is working in the EHR, has a patient chart open, and clicks a button or menu item to launch your app. The EHR passes the patient context to your app automatically.

Authorization URL parameters:

{authorization_endpoint}?
  response_type=code&
  client_id={your-client-id}&
  redirect_uri={your-redirect-url}&
  scope=launch openid fhirUser patient/*.read&
  state={random-state-value}&
  aud={fhir-base-url}&
  launch={launch-token-from-ehr}

Key parameter: launch — this token links the authorization request to the specific EHR session and patient context.

Best for: Clinical workflow applications, clinical decision support tools, order entry assistants, documentation aids, prior authorization tools — any app that needs to operate in the context of a specific patient encounter.

Standalone Launch

How it works: Your app launches independently — from a browser bookmark, a mobile app icon, or any other entry point outside the EHR. The user authenticates and then selects a patient.

Authorization URL parameters:

{authorization_endpoint}?
  response_type=code&
  client_id={your-client-id}&
  redirect_uri={your-redirect-url}&
  scope=launch/patient openid fhirUser patient/*.read&
  state={random-state-value}&
  aud={fhir-base-url}

Key difference: Instead of the launch parameter, the scope includes launch/patient, which tells the authorization server to present a patient picker during the authorization flow.

Best for: Patient-facing apps, administrative tools, reporting applications, data analytics dashboards — apps that do not need to be embedded within the EHR clinical workflow.

Which Launch Type Should You Support?

Most apps should support both launch types. EHR launch for clinical workflow integration and standalone launch for use outside the EHR. The authorization logic is nearly identical — the only difference is how patient context is obtained (automatically from EHR vs patient picker).


OAuth 2.0 Authorization Flow

SMART on FHIR uses OAuth 2.0 Authorization Code Grant with Proof Key for Code Exchange (PKCE) for public clients.

Step-by-Step Authorization Flow

Step 1: Discover OAuth endpoints

const smartConfig = await fetch(`${fhirBaseUrl}/.well-known/smart-configuration`)
  .then(r => r.json());

const authUrl = smartConfig.authorization_endpoint;
const tokenUrl = smartConfig.token_endpoint;

Step 2: Generate PKCE code verifier and challenge

function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return btoa(String.fromCharCode(...array))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

async function generateCodeChallenge(verifier) {
  const hash = await crypto.subtle.digest('SHA-256', 
    new TextEncoder().encode(verifier));
  return btoa(String.fromCharCode(...new Uint8Array(hash)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

Step 3: Redirect to authorization endpoint

const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Store codeVerifier in session for later use

const authParams = new URLSearchParams({
  response_type: 'code',
  client_id: YOUR_CLIENT_ID,
  redirect_uri: YOUR_REDIRECT_URI,
  scope: 'launch openid fhirUser patient/*.read',
  state: generateRandomState(),
  aud: fhirBaseUrl,
  launch: launchToken, // only for EHR launch
  code_challenge: codeChallenge,
  code_challenge_method: 'S256'
});

window.location.href = `${authUrl}?${authParams}`;

Step 4: Handle the redirect callback

After the user authenticates, the authorization server redirects to your redirect URI with an authorization code:

https://your-app.com/callback?code=AUTH_CODE&state=YOUR_STATE

Step 5: Exchange code for access token

const tokenResponse = await fetch(tokenUrl, {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    redirect_uri: YOUR_REDIRECT_URI,
    client_id: YOUR_CLIENT_ID,
    code_verifier: storedCodeVerifier
  })
}).then(r => r.json());

const accessToken = tokenResponse.access_token;
const patientId = tokenResponse.patient; // patient context
const encounterId = tokenResponse.encounter; // encounter context (if available)
const idToken = tokenResponse.id_token; // user identity (if openid scope requested)

Step 6: Use the access token for FHIR API calls

const patient = await fetch(`${fhirBaseUrl}/Patient/${patientId}`, {
  headers: { 'Authorization': `Bearer ${accessToken}` }
}).then(r => r.json());

Token Refresh

Access tokens are short-lived (5–60 minutes depending on the EHR). If the token response includes a refresh_token, use it to obtain new access tokens without re-authorization:

const refreshResponse = await fetch(tokenUrl, {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: storedRefreshToken,
    client_id: YOUR_CLIENT_ID
  })
}).then(r => r.json());

FHIR Scopes and Permissions

SMART scopes define what data your app can access. Scopes follow a specific format:

Patient-Level Scopes

Format: patient/{resource-type}.{permission}

  • patient/Patient.read — read the patient’s demographic data
  • patient/Observation.read — read lab results and vitals
  • patient/MedicationRequest.read — read medication orders
  • patient/Condition.read — read diagnoses and problems
  • patient/AllergyIntolerance.read — read allergies
  • patient/Procedure.read — read procedures
  • patient/Immunization.read — read immunizations
  • patient/DocumentReference.read — read clinical documents
  • patient/Encounter.read — read encounter history
  • patient/*.read — read all resource types for the patient
  • patient/Observation.write — write observations (vitals, patient-reported data)

User-Level Scopes

Format: user/{resource-type}.{permission}

User-level scopes grant access based on the logged-in user’s permissions within the EHR — not limited to a single patient.

  • user/Patient.read — read any patient the user has access to
  • user/Practitioner.read — read practitioner information
  • user/*.read — read all resources the user has permission to access

System-Level Scopes

Format: system/{resource-type}.{permission}

System-level scopes are for backend services (no user interaction). Used with the Client Credentials grant.

  • system/Patient.read — read all patients
  • system/*.read — read all resources
  • system/Group.read — required for Bulk Data Export

Special Scopes

  • launch — indicates EHR launch (receive launch context)
  • launch/patient — indicates standalone launch (patient picker)
  • openid — request OpenID Connect identity token
  • fhirUser — include the FHIR user reference in the token

Scope Best Practices

Request minimum necessary scopes. Only request access to the specific resource types your app needs. An app that only displays medications should request patient/MedicationRequest.read, not patient/*.read. EHR authorization servers may reject overly broad scope requests, and patients are more likely to consent to specific, limited access.

Separate read and write scopes. If your app only needs to read data, do not request write scopes. Write access requires additional review and approval from EHR vendors.


Building Your First SMART on FHIR App

Prerequisites

  • A web application (React, Angular, Vue, or vanilla JavaScript)
  • A registered application on your target EHR’s developer portal
  • Access to the EHR’s sandbox environment
  • Understanding of OAuth 2.0 and FHIR R4

Using the SMART on FHIR JavaScript Client Library

The fhirclient JavaScript library handles the SMART launch flow, authorization, and FHIR API calls. It is the recommended approach for web-based SMART apps.

Install:

npm install fhirclient

EHR Launch — launch.html:

import FHIR from 'fhirclient';

FHIR.oauth2.authorize({
  clientId: 'your-client-id',
  scope: 'launch openid fhirUser patient/Patient.read patient/Observation.read patient/MedicationRequest.read patient/Condition.read patient/AllergyIntolerance.read',
  redirectUri: '/app'
});

App page — app.html:

import FHIR from 'fhirclient';

async function loadApp() {
  const client = await FHIR.oauth2.ready();
  
  // Get patient demographics
  const patient = await client.patient.read();
  console.log(`Patient: ${patient.name[0].given[0]} ${patient.name[0].family}`);
  console.log(`DOB: ${patient.birthDate}`);
  console.log(`Gender: ${patient.gender}`);

  // Get active conditions
  const conditions = await client.request(
    `Condition?patient=${client.patient.id}&clinical-status=active`
  );
  
  // Get active medications
  const medications = await client.request(
    `MedicationRequest?patient=${client.patient.id}&status=active`
  );

  // Get recent lab results
  const labs = await client.request(
    `Observation?patient=${client.patient.id}&category=laboratory&_sort=-date&_count=20`
  );

  // Get allergies
  const allergies = await client.request(
    `AllergyIntolerance?patient=${client.patient.id}`
  );

  // Render data in your UI
  renderPatientSummary(patient, conditions, medications, labs, allergies);
}

loadApp();

Handling FHIR Bundle Responses

FHIR search queries return Bundle resources containing the matching entries:

const bundle = await client.request(
  `Condition?patient=${client.patient.id}&clinical-status=active`
);

if (bundle.entry) {
  const conditions = bundle.entry.map(e => e.resource);
  conditions.forEach(condition => {
    const code = condition.code?.coding?.[0]?.display || 'Unknown';
    const icd10 = condition.code?.coding?.find(c => 
      c.system === 'http://hl7.org/fhir/sid/icd-10-cm'
    )?.code;
    console.log(`${code} (${icd10})`);
  });
}

Handling Pagination

For large result sets, follow the Bundle’s pagination links:

async function getAllResults(client, url) {
  let results = [];
  let bundle = await client.request(url);
  
  while (bundle) {
    if (bundle.entry) {
      results = results.concat(bundle.entry.map(e => e.resource));
    }
    const nextLink = bundle.link?.find(l => l.relation === 'next');
    bundle = nextLink ? await client.request(nextLink.url) : null;
  }
  
  return results;
}

const allLabs = await getAllResults(client, 
  `Observation?patient=${client.patient.id}&category=laboratory`
);

Reading Clinical Data with FHIR APIs

Patient Demographics

const patient = await client.patient.read();

const name = `${patient.name[0].given.join(' ')} ${patient.name[0].family}`;
const dob = patient.birthDate;
const gender = patient.gender;
const mrn = patient.identifier?.find(id => 
  id.type?.coding?.[0]?.code === 'MR'
)?.value;
const phone = patient.telecom?.find(t => t.system === 'phone')?.value;
const email = patient.telecom?.find(t => t.system === 'email')?.value;
const address = patient.address?.[0];

Vital Signs

const vitals = await client.request(
  `Observation?patient=${client.patient.id}&category=vital-signs&_sort=-date&_count=10`
);

vitals.entry?.forEach(e => {
  const obs = e.resource;
  const type = obs.code?.coding?.[0]?.display;
  const value = obs.valueQuantity?.value;
  const unit = obs.valueQuantity?.unit;
  const date = obs.effectiveDateTime;
  console.log(`${type}: ${value} ${unit} (${date})`);
});

Lab Results

const labs = await client.request(
  `Observation?patient=${client.patient.id}&category=laboratory&_sort=-date&_count=50`
);

labs.entry?.forEach(e => {
  const obs = e.resource;
  const testName = obs.code?.coding?.[0]?.display;
  const loinc = obs.code?.coding?.find(c => 
    c.system === 'http://loinc.org'
  )?.code;
  const value = obs.valueQuantity?.value || obs.valueString;
  const unit = obs.valueQuantity?.unit || '';
  const refLow = obs.referenceRange?.[0]?.low?.value;
  const refHigh = obs.referenceRange?.[0]?.high?.value;
  const abnormal = obs.interpretation?.[0]?.coding?.[0]?.code; // H, L, A, etc.
  const date = obs.effectiveDateTime;
});

Medications

const meds = await client.request(
  `MedicationRequest?patient=${client.patient.id}&status=active`
);

meds.entry?.forEach(e => {
  const med = e.resource;
  const drugName = med.medicationCodeableConcept?.coding?.[0]?.display 
    || med.medicationCodeableConcept?.text;
  const rxnorm = med.medicationCodeableConcept?.coding?.find(c => 
    c.system === 'http://www.nlm.nih.gov/research/umls/rxnorm'
  )?.code;
  const dosage = med.dosageInstruction?.[0]?.text;
  const prescriber = med.requester?.display;
  const dateWritten = med.authoredOn;
});

Clinical Notes (OpenNotes)

const notes = await client.request(
  `DocumentReference?patient=${client.patient.id}&type=clinical-note&_sort=-date&_count=10`
);

notes.entry?.forEach(e => {
  const doc = e.resource;
  const noteType = doc.type?.coding?.[0]?.display;
  const date = doc.date;
  const author = doc.author?.[0]?.display;
  
  // Get the actual note content
  const attachment = doc.content?.[0]?.attachment;
  if (attachment?.data) {
    const noteText = atob(attachment.data); // Base64 decode
  } else if (attachment?.url) {
    // Fetch the note content from the URL
    const noteContent = await client.request(attachment.url);
  }
});

Writing Data Back to the EHR

Write operations require specific write scopes and additional approval from EHR vendors. Common write use cases:

Creating an Observation (Patient-Reported Data)

const observation = {
  resourceType: 'Observation',
  status: 'final',
  category: [{
    coding: [{
      system: 'http://terminology.hl7.org/CodeSystem/observation-category',
      code: 'vital-signs',
      display: 'Vital Signs'
    }]
  }],
  code: {
    coding: [{
      system: 'http://loinc.org',
      code: '85354-9',
      display: 'Blood pressure panel'
    }]
  },
  subject: { reference: `Patient/${client.patient.id}` },
  effectiveDateTime: new Date().toISOString(),
  component: [
    {
      code: { coding: [{ system: 'http://loinc.org', code: '8480-6', display: 'Systolic BP' }] },
      valueQuantity: { value: 120, unit: 'mmHg', system: 'http://unitsofmeasure.org', code: 'mm[Hg]' }
    },
    {
      code: { coding: [{ system: 'http://loinc.org', code: '8462-4', display: 'Diastolic BP' }] },
      valueQuantity: { value: 80, unit: 'mmHg', system: 'http://unitsofmeasure.org', code: 'mm[Hg]' }
    }
  ]
};

const created = await client.create(observation);

Creating a DocumentReference

const docRef = {
  resourceType: 'DocumentReference',
  status: 'current',
  type: {
    coding: [{
      system: 'http://loinc.org',
      code: '34133-9',
      display: 'Summary of episode note'
    }]
  },
  subject: { reference: `Patient/${client.patient.id}` },
  date: new Date().toISOString(),
  content: [{
    attachment: {
      contentType: 'text/plain',
      data: btoa('Clinical assessment summary text here...')
    }
  }]
};

const created = await client.create(docRef);

Write Operation Considerations

  • Not all EHR platforms support writes for all resource types
  • Write scopes must be explicitly requested and approved
  • Data validation is stricter for writes — ensure correct coding systems, required fields, and proper references
  • Test writes extensively in sandbox before production
  • Some EHRs require writes to go through clinical review before appearing in the chart

CDS Hooks Integration

CDS Hooks extend SMART on FHIR by enabling your application to provide clinical decision support at specific workflow trigger points within the EHR.

How CDS Hooks Work

  1. The EHR registers your CDS service endpoint
  2. At defined workflow points (hooks), the EHR sends a request to your service with context data
  3. Your service evaluates the context and returns decision support cards
  4. The EHR displays your cards to the clinician within their workflow

Supported Hook Types

HookTriggerUse Case
patient-viewClinician opens a patient chartCare gap alerts, risk scores, prior visit summaries
order-selectClinician selects an orderDrug interaction warnings, formulary checks, prior auth requirements
order-signClinician signs ordersFinal validation, cost alerts, guideline compliance checks
encounter-startNew encounter beginsScreening reminders, protocol suggestions
encounter-dischargeDischarge workflow initiatedDischarge checklist, follow-up scheduling reminders
appointment-bookAppointment being scheduledPre-visit preparation reminders, eligibility checks

CDS Hooks Service Endpoint

Your service exposes a discovery endpoint and a hook endpoint:

Discovery: GET https://your-cds-service.com/cds-services

{
  "services": [{
    "hook": "patient-view",
    "title": "Diabetes Risk Assessment",
    "description": "Evaluates patient risk factors for Type 2 diabetes",
    "id": "diabetes-risk",
    "prefetch": {
      "patient": "Patient/{{context.patientId}}",
      "conditions": "Condition?patient={{context.patientId}}&clinical-status=active",
      "labs": "Observation?patient={{context.patientId}}&category=laboratory&code=4548-4&_sort=-date&_count=1"
    }
  }]
}

Hook invocation: POST https://your-cds-service.com/cds-services/diabetes-risk

The EHR sends context and prefetched data. Your service responds with cards:

{
  "cards": [{
    "summary": "HbA1c trending upward — consider diabetes screening",
    "detail": "Patient's last HbA1c was 6.2% (3 months ago). Combined with BMI of 31 and family history, this patient meets ADA screening criteria.",
    "indicator": "warning",
    "source": {
      "label": "Taction Diabetes Risk Engine",
      "url": "https://your-app.com/info"
    },
    "suggestions": [{
      "label": "Order HbA1c test",
      "actions": [{
        "type": "create",
        "description": "Order HbA1c",
        "resource": {
          "resourceType": "ServiceRequest",
          "code": { "coding": [{ "system": "http://loinc.org", "code": "4548-4" }] },
          "subject": { "reference": "Patient/12345" }
        }
      }]
    }],
    "links": [{
      "label": "View full risk assessment",
      "url": "https://your-app.com/launch",
      "type": "smart"
    }]
  }]
}

CDS Hooks Best Practices

Minimize alert fatigue. Only return cards when clinically significant. If your service returns cards for every patient, clinicians will ignore all of them. Target a card rate below 15–20% of patient encounters.

Keep cards concise. The summary should be actionable in one sentence. Use detail for supporting evidence. Clinicians spend seconds, not minutes, reviewing CDS cards.

Provide actionable suggestions. Cards with specific, one-click actions (order a test, update a diagnosis) are acted upon 3–5x more frequently than informational-only cards.

Respond fast. EHRs impose timeout limits on CDS Hook calls (typically 5–10 seconds). Your service must respond within this window. Pre-compute risk scores and cache results where possible.


SMART on FHIR with Epic

Epic’s SMART on FHIR implementation is the most widely deployed in the US hospital market.

Registration: Register your app through Epic’s App Orchard/Showroom. Specify SMART launch type, required scopes, and redirect URIs.

Epic-specific considerations:

  • Epic uses its own authorization server — OAuth endpoints are Epic-specific per customer instance
  • Epic supports both EHR launch (from Hyperspace/MyChart) and standalone launch
  • Epic’s FHIR server enforces strict scope validation — requesting unsupported scopes will cause authorization failure
  • Epic returns patient, encounter, and fhirUser context in the token response
  • Epic supports CDS Hooks for patient-view, order-select, and order-sign
  • Epic’s sandbox (available through App Orchard) contains synthetic data for testing

Epic-specific scopes: Epic supports standard SMART scopes plus Epic-specific extensions for certain resource types. Always check Epic’s FHIR documentation for the exact scope syntax for each resource.


SMART on FHIR with Oracle Health

Oracle Health (Cerner) Millennium supports SMART on FHIR through its Ignite API platform.

Registration: Register through the Oracle Health developer portal. Create an application with SMART launch capabilities.

Oracle Health-specific considerations:

  • Millennium’s authorization server handles OAuth flows
  • Oracle Health supports both EHR launch (from PowerChart) and standalone launch
  • Oracle Health’s multi-tenant sandbox provides immediate access for development
  • Oracle Health returns Millennium-specific identifiers in the patient context — map these to your application’s data model
  • CDS Hooks support is available for patient-view, order-select, and order-sign
  • Oracle Health is transitioning API management to Oracle Cloud — new apps may encounter Oracle API Gateway requirements

SMART on FHIR with Athenahealth

Athenahealth supports SMART on FHIR alongside its proprietary REST APIs.

Registration: Register through the athenahealth developer portal. Configure SMART launch for Marketplace integration.

Athenahealth-specific considerations:

  • Athenahealth’s cloud-native architecture means consistent SMART behavior across all customer practices
  • Athenahealth supports EHR launch (from athenaOne) and standalone launch
  • Athenahealth’s FHIR implementation covers clinical and some administrative resources — revenue cycle data is primarily accessible through the proprietary REST API
  • No per-site configuration required — SMART app works for all athenahealth customers once approved
  • Marketplace embedded apps can combine SMART launch with athenahealth’s proprietary embedding framework

Backend Services and System-Level Access

For applications that need to access FHIR data without user interaction — background synchronization, analytics pipelines, population health tools — SMART defines the Backend Services authorization profile.

Client Credentials with Asymmetric Keys

Backend services authenticate using JWT assertions signed with a private key:

const jwt = createSignedJWT({
  iss: YOUR_CLIENT_ID,
  sub: YOUR_CLIENT_ID,
  aud: tokenUrl,
  exp: Math.floor(Date.now() / 1000) + 300,
  jti: generateUniqueId()
}, YOUR_PRIVATE_KEY);

const tokenResponse = await fetch(tokenUrl, {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    scope: 'system/*.read',
    client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
    client_assertion: jwt
  })
}).then(r => r.json());

Bulk Data Export

Backend services can use the FHIR Bulk Data Export specification to export large datasets:

// Initiate export
const exportResponse = await fetch(`${fhirBaseUrl}/Patient/$export`, {
  method: 'GET',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Accept': 'application/fhir+json',
    'Prefer': 'respond-async'
  }
});

const statusUrl = exportResponse.headers.get('Content-Location');

// Poll for completion
let status;
do {
  await new Promise(resolve => setTimeout(resolve, 10000)); // wait 10 seconds
  status = await fetch(statusUrl, {
    headers: { 'Authorization': `Bearer ${accessToken}` }
  });
} while (status.status === 202);

// Download results
const result = await status.json();
const fileUrls = result.output.map(f => f.url);

Backend services are essential for healthcare data analytics platforms and remote patient monitoring systems that need ongoing data access without clinician interaction.


Security and HIPAA Considerations

Access Token Security

  • Never expose access tokens in client-side JavaScript logs, URLs, or browser storage
  • Use short-lived access tokens (match the EHR’s token lifetime)
  • Implement token refresh logic for long-running sessions
  • Revoke tokens when the user logs out or the session ends
  • Use HTTPS for all API calls — never transmit tokens over unencrypted connections

PKCE Requirements

Public clients (browser-based apps, mobile apps) must use PKCE (Proof Key for Code Exchange) to prevent authorization code interception attacks. The code_verifier and code_challenge parameters in the OAuth flow implement this protection.

Confidential clients (server-side apps with a client secret) should also use PKCE as a defense-in-depth measure.

Data Handling

  • Do not store PHI in the browser beyond the current session
  • If your app caches FHIR data locally, encrypt it and clear it when the session ends
  • Audit log all FHIR API calls your app makes (which user, which patient, which data)
  • Implement automatic session timeout consistent with HIPAA requirements (15–30 minutes of inactivity)

CORS and Content Security Policy

EHR FHIR servers typically support CORS for browser-based SMART apps. However, some EHR implementations require server-side API calls (your backend proxies FHIR requests) rather than direct browser-to-FHIR calls. Test CORS behavior in each EHR’s sandbox.

If your SMART app runs in an iframe within the EHR, Content Security Policy headers may restrict which external resources your app can load. Test iframe behavior early in development.


Testing and Debugging

Public SMART on FHIR Sandboxes

SandboxURLDescription
SMART Health IT Sandboxlaunch.smarthealthit.orgOpen sandbox for testing SMART launch flows
Logica Health Sandboxsandbox.logicahealth.orgFull FHIR R4 sandbox with synthetic data
Epic App Orchard SandboxThrough Epic developer portalEpic-specific FHIR sandbox
Oracle Health SandboxThrough Cerner Code portalMillennium-specific sandbox
Athenahealth PreviewThrough athenahealth developer portalAthenahealth-specific sandbox

Debugging Tools

SMART App Launcher: The SMART Health IT Launcher (launch.smarthealthit.org) allows you to simulate EHR launches without an actual EHR. Configure patient context, user context, and supported scopes to test your app’s launch flow.

FHIR Request Inspector: Log all FHIR API requests and responses during development. The fhirclient library supports debug mode that logs HTTP requests.

Token Decoder: Use jwt.io to decode and inspect access tokens and ID tokens. Verify that scopes, patient context, and token expiration are correct.

Browser Developer Tools: Monitor network requests to track OAuth redirects, FHIR API calls, and error responses. The Authorization Code flow involves multiple redirects that can be difficult to follow without network monitoring.

Common Debugging Issues

“Invalid scope” error during authorization: The requested scope is not supported by the EHR or not approved for your application. Check the SMART configuration endpoint for scopes_supported and verify your registered scopes match.

“Invalid redirect URI” error: The redirect URI in your authorization request does not match exactly what you registered with the EHR. Check for trailing slashes, protocol differences (http vs https), and port numbers.

Empty patient context: The token response does not include a patient parameter. Verify your scope includes launch (for EHR launch) or launch/patient (for standalone launch).

CORS errors on FHIR API calls: The EHR’s FHIR server is blocking browser-to-server requests. Use a backend proxy or confirm that the EHR supports CORS for your application’s origin.


Deployment and EHR Marketplace Listing

Hosting Requirements

  • Your SMART app must be hosted on HTTPS (TLS 1.2+)
  • The app must be available at a stable URL (the launch URL registered with the EHR)
  • For embedded (iframe) apps, ensure your server sends appropriate X-Frame-Options and Content-Security-Policy headers
  • High availability is expected — EHR launch failures create clinical workflow disruption
  • Target sub-2-second load time for the initial launch

EHR Marketplace Submission

Each EHR has its own marketplace and approval process:

Epic App Orchard/Showroom: Submit through the Epic developer portal. Expect 2–4 months for review. Each Epic customer site must independently activate your app. See our Epic integration guide for detailed steps.

Oracle Health (Cerner Code): Submit through the Oracle Health developer portal. Expect 4–8 weeks for review. Per-site activation required. See our Oracle Health integration guide for details.

Athenahealth Marketplace: Submit through the athenahealth developer portal. Expect 6–12 weeks for review. No per-site activation — approved apps work for all athenahealth customers. See our athenahealth integration guide for the Marketplace process.

Multi-EHR Deployment Architecture

For apps supporting multiple EHR platforms, use a configuration-driven architecture:

  • Store EHR-specific configuration (FHIR base URL, client ID, supported scopes) per deployment target
  • Use the SMART discovery mechanism to dynamically resolve OAuth endpoints
  • Handle EHR-specific FHIR resource variations in a translation layer
  • Test against each EHR’s sandbox independently before cross-platform deployment

Common Mistakes and How to Avoid Them

Hardcoding FHIR server URLs. Your app must dynamically discover the FHIR server URL from the iss parameter during launch. Hardcoded URLs break when deploying to different EHR instances or when EHR infrastructure changes.

Ignoring token expiration. Access tokens expire. Implement token refresh logic and handle 401 (Unauthorized) responses gracefully by refreshing the token and retrying the request.

Not handling missing data. FHIR resources have optional fields. A Patient resource may not have a phone number. A Condition may not have an onset date. An Observation may not have a reference range. Your app must handle null/undefined values for every field without crashing.

Requesting too many scopes. Broad scopes (patient/*.read) may be rejected by EHR authorization servers or raise flags during marketplace review. Request only the specific resource types your app needs.

Not supporting both launch types. Building only EHR launch or only standalone launch limits your app’s utility. Support both with shared application logic.

Slow app load time. SMART apps launch in clinical workflows where seconds matter. Optimize your initial load — defer non-critical data fetches, use loading states, and lazy-load components. If your app takes more than 3 seconds to display useful content after launch, clinicians will not use it.

Not testing with real EHR sandboxes. Public FHIR sandboxes behave differently from Epic, Oracle Health, and athenahealth sandboxes. Always test against the specific EHR sandbox before submitting for marketplace review.

Ignoring CDS Hooks response time limits. EHRs time out CDS Hook requests after 5–10 seconds. If your CDS service is slow, the EHR discards your response and the clinician never sees your cards. Pre-compute results and cache aggressively.


Next Steps

SMART on FHIR is the standard for building interoperable healthcare applications. Mastering this framework gives your application access to every major EHR platform in the United States through a single technical architecture.

If you are building a SMART on FHIR application and need guidance on multi-EHR deployment, CDS Hooks integration, or marketplace approval, connect with our interoperability team or explore our healthcare interoperability services.


Related Resources:


This tutorial was developed by the healthcare interoperability team at Taction Software, based on production SMART on FHIR implementations deployed across Epic, Oracle Health, and athenahealth environments for US healthcare organizations and health tech companies.

Frequently Asked Questions

Do I need to build separate apps for Epic, Cerner, and athenahealth?

No. A single SMART on FHIR app can work across all three platforms. The SMART launch framework and FHIR R4 API are standardized. You will need separate app registrations with each EHR vendor, and you should test against each vendor’s sandbox, but the core application code is shared.

Can SMART on FHIR apps write data to the EHR?

Yes, but write access is more restricted than read access. You must request write scopes, and the EHR vendor must approve those scopes during the marketplace review process. Common write use cases include creating Observations (patient-reported data, device readings), creating DocumentReferences (clinical documents), and creating ServiceRequests (orders). Each EHR has different write capabilities — check the vendor’s FHIR documentation.

How do I handle offline access or background data sync?

SMART on FHIR’s standard OAuth flow is designed for interactive sessions. For background access, use the Backend Services authorization profile with client credentials. This requires a separate app registration with system-level scopes and is subject to additional approval from EHR vendors.

What programming languages can I use?

Any language that can handle HTTP requests and OAuth 2.0. JavaScript (with the fhirclient library) is most common for web-based SMART apps. Python, Java, C#, and Swift all have FHIR client libraries. Mobile SMART apps use Swift (iOS) or Kotlin (Android) with FHIR client libraries.

How do SMART on FHIR apps handle HIPAA compliance?

Your SMART app must implement all standard HIPAA safeguards: encrypt data in transit (HTTPS), minimize local data storage, implement session timeout, audit log all data access, and handle PHI according to your BAA. The SMART framework handles authorization and scoping, but your app is responsible for everything that happens after you receive the data.

What is the difference between SMART on FHIR and plain FHIR?

FHIR is the data standard — it defines resources, search parameters, and API conventions. SMART on FHIR adds the launch framework (how apps start and receive context from the EHR) and the authorization framework (how apps get permission to access data). You can use FHIR APIs without SMART (direct API calls with API keys or service accounts), but SMART is required for apps that launch from within the EHR and need user-context-aware authorization.

How long does it take to build a SMART on FHIR app?

A simple read-only SMART app (display patient data) can be built in 2–4 weeks. A full-featured clinical workflow app with CDS Hooks and write operations typically takes 2–5 months. Add 2–4 months for EHR marketplace review and approval per vendor. Taction Software’s multi-EHR SMART app deployments average 4–7 months from development start to production availability across all three major EHR platforms.

Abhishek Sharma

Writer & Blogger

    contact sidebar - Taction Software

    Let’s Achieve Digital
    Excellence Together

    Your Next Big Project Starts Here

    Explore how we can streamline your business with custom IT solutions or cutting-edge app development.

    Why connect with us?

      What is 2 x 8 ? Refresh icon

      Wait! Your Next Big Project Starts Here

      Don’t leave without exploring how we can streamline your business with custom IT solutions or cutting-edge app development.

      Why connect with us?

        What is 1 + 2 ? Refresh icon