The ModMed Certified (ONC) FHIR API uses OAuth 2.0 with the SMART App Launch framework to authorize requests. Your application obtains an access token, then includes it as a Bearer token on every FHIR call. Which flow you use depends on the kind of application you're building — see App Types below.
Production onlyThe Certified FHIR API does not have a public sandbox. Authorization and API calls run against the production environment.
Endpoints
| Purpose | URL |
|---|---|
| Authorization | https://sso.ema.md/auth/realms/fhir/protocol/openid-connect/auth |
| Token | https://sso.ema.md/auth/realms/fhir/protocol/openid-connect/token |
FHIR base (aud) | https://fhirmp.mmi.prod.fhir.ema-api.com/fhir/r4 |
| Discovery | https://fhirmp.mmi.prod.fhir.ema-api.com/fhir/r4/.well-known/smart-configuration |
The same token endpoint serves both flows, but client authentication differs by grant type: authorization_code/refresh_token use client_secret_post (send client_id/client_secret in the body); client_credentials (Bulk FHIR) uses private_key_jwt instead — no client_secret at all. See Client Credentials below. PKCE (S256) is supported and recommended for user-facing apps.
App Types
Before registering, decide what type of application you're building — it determines how users authenticate and which scopes you request.
- Bulk FHIR (system / background) — If you're working with a practice and need access to their providers' and patients' clinical data at the practice level, build a Bulk FHIR application. A practice Admin adds your
ClientIdto their practice once, and your application accesses data as needed using theclient_credentialsflow, authenticated with a signed JWT (private_key_jwt) — not a client_secret. No per-user authentication. - Patient apps — Require a Patient at the practice (with valid Patient Portal username/password) to authenticate. After the first Standalone authentication, a link is placed inside the patient portal for repeated access and for the patient to manage or disable access.
- Provider apps — Require a Provider at the practice to authenticate with the same credentials they use for the EHR. After authenticating, a link is placed inside their EHR instance (location varies by scope) to launch your app from within the EHR.
- Patient and Provider apps — Handle authentication for both patients and providers.
For all Patient and Provider app types, the vendor must provide the initial Standalone authentication mechanism. Once a user authenticates via the Standalone flow, an in-context EHR Launch link is placed in their UI for ongoing use.
Authorization flows
Authorization Code (with PKCE) — Patient & Provider apps
- Redirect the user's browser to the authorization endpoint with:
response_type=code,client_id,redirect_uri,scope(space-separated),state,aud(the FHIR base URL above), andcode_challenge+code_challenge_method=S256. - The user authenticates — Patient Portal credentials for patient apps, EHR credentials for provider apps. For EHR Launch, the app is started from within EMA and receives a
launchparameter (include thelaunchscope). - The server redirects back to your
redirect_uriwith acodeand yourstate. - Exchange the code at the token endpoint:
grant_type=authorization_code,code,redirect_uri,code_verifier,client_id,client_secret. - You receive an
access_token(plus anid_tokenwhenopenidis requested, and arefresh_tokenwhenonline_access/offline_accessis granted). Send the access token asAuthorization: Bearer <token>on FHIR calls.
Client Credentials — Bulk FHIR / background apps
No browser, no user interaction, and no client_secret. Your client is registered with a JWKS containing your public key; each token request authenticates with a client_assertion — a JWT signed with your matching private key — instead of a shared secret (private_key_jwt).
POST to the token endpoint with:
grant_type=client_credentialsclient_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearerclient_assertion— a JWT signed with your private key, with claimsiss/sub= yourclient_id,aud= this token endpoint URL, plusjti/iat/exp(a short expiry — 5 minutes is typical). Confirmed working signing algorithm: ES384.scope— required on this grant (space-separatedsystem/*.rsscopes); it isn't implied the way it can be on the other grants.
Note there's no client_id field in the body either — the client is identified via the client_assertion JWT's iss/sub claims. Use the returned access_token as a Bearer token.
Scopes
Request only the scopes your application needs. Scopes fall into four groups.
Context scopes — control what gets put in the token
| Scope | Standalone | EHR Launch | What it does |
|---|---|---|---|
| launch/patient | Triggers patient picker | Passes patient from EHR context | Puts patient claim in the token |
| launch/encounter | Rarely useful | Passes encounter from EHR | Puts encounter claim in token |
| launch | Never | Always required | Tells auth server to honor the EHR's launch context |
Identity scopes — control what you know about the logged-in user
| Scope | When to use | What it does |
|---|---|---|
| openid | Interactive apps only — not applicable to client_credentials (no id_token is issued on that grant) | Required for OIDC — enables id_token |
| profile | When you need user name/email | Adds basic profile claims to id_token |
| fhirUser | When you need to look up the user as a FHIR resource | Adds a fhirUser claim — a URL like Practitioner/abc123 you can GET |
Session scopes — control token lifetime
| Scope | When to use | What it does |
|---|---|---|
| online_access | Most interactive apps | Refresh token that expires when the user's EMA session ends |
| offline_access | Interactive background/long-running apps | Refresh token that doesn't expire with the session — not applicable to client_credentials, which reissues a new token via a fresh signed assertion instead of refreshing |
Resource scopes — control what FHIR data you can read
| Scope | When to use | What it does |
|---|---|---|
| patient/Resource.rs | Patient context apps | Read/search that resource scoped to the token's patient — can't read other patients' data |
| user/Resource.rs | Provider context apps | Read/search that resource as the logged-in user — broader access across patients they can see |
| system/Resource.rs | Bulk FHIR / background apps (client_credentials) | Read/search that resource across the practice, with no user or patient context — required on the client_credentials grant |
Recommended scopes by app type
| App Type | Required | Makes sense | Doesn't make sense |
|---|---|---|---|
| Standalone (patient picker) | openid, launch/patient | fhirUser, online_access, patient/*.rs | launch, launch/encounter, user/*.rs, system/*.rs |
| EHR Launch — Patient | openid, launch, launch/patient | fhirUser, online_access, patient/*.rs | launch/encounter, user/*.rs, system/*.rs |
| EHR Launch — Provider | openid, launch, launch/encounter | fhirUser, online_access, user/*.rs | launch/patient, patient/*.rs, system/*.rs |
| EHR Launch — Patient + Provider | openid, launch, launch/patient, launch/encounter | fhirUser, online_access, patient/.rs,user/.rs | system/*.rs |
| Bulk FHIR / background (client_credentials) | system/*.rs | — | openid, offline_access, launch*, patient/*.rs, user/*.rs |
Register
Haven't registered yet? See Become a ModMed Certified FHIR API Vendor to choose your app type and configure your application.

