ModMed
Access
Section titled “Access”- No self-service portal: ModMed support enables API access per practice and issues a per-practice FHIR R4 endpoint (each practice has its own subdomain). There is no sandbox.
- SMART Backend Services with a JWT signed ES384. Scopes use the SMART v2 form, for example
system/Observation.rs. - The token URL is
<base>/auth/realms/fhir/protocol/openid-connect/tokenon the practice’s own subdomain — not the older central sign-on realm. - ModMed has had more than one FHIR app registration over time. If you hold several, pin each practice to the app it was enabled for, so a re-saved connection cannot silently switch apps.
- Build URLs carefully: a double slash (
//) in the path returns 400 or 502.
Extraction approach
Section titled “Extraction approach”A combination that has worked for five measures in one run:
- Bulk
Patient/$exportwithout_since— the full roster. - Bulk
Encounter/$exportwith_since=<year>-01-01— the active cohort and attribution. - Per-patient search for
Conditionand year-filteredObservation(bulk does not carry everything), plusDocumentReference,MedicationRequest,ServiceRequestandProcedurewhen a measure needs them. GET /Practitioner/<id>for provider names and NPI.
Retry kickoffs a few times with increasing waits, and poll every 15 seconds for up to several hours.
Known behaviour
Section titled “Known behaviour”Bulk manifests are unstable
Section titled “Bulk manifests are unstable”- A 200 can carry an empty or partial
outputlist that keeps filling after the 200. - Each file holds at most 1,000 resources, so a truncated export shows up as an exact multiple of 1,000 with no remainder file.
- Only accept a manifest once its file count has stopped changing across several consecutive polls.
Search fallbacks
Section titled “Search fallbacks”- When bulk never completes, walk the search API in pages of 1,000.
_lastUpdatedis ignored onEncounter, so apply the year window in your own code.
Date searches
Section titled “Date searches”A bare year (date=2026) returns 200 with total=0. Use prefixed forms: date=ge2026-01-01,
or a ge/le pair.
Paging
Section titled “Paging”Pages are fixed at 100 entries, and the Bundle total reports the page size, not the number of
matches. Always walk next.
Practitioners and NPI
Section titled “Practitioners and NPI”- Bulk
Practitionerexport returns zero rows, butGET /Practitioner/<id>returns the name and NPI. - Some practices publish NPI as a data-absent reason. Keep a maintained map from EHR provider id to NPI, and extend it whenever a run reports an unmapped provider.
- TIN and practice identifiers are not in the API.
Attachments
Section titled “Attachments”Attachment URLs come in two forms:
- a relative
/Binary/<id>— returns JSON with base64 content; send your bearer token; - an absolute pre-signed cloud-storage URL — fetch it without the bearer token (sending both returns 400 “Only one auth mechanism allowed”). Allowlist the hosts you will fetch from.
Depression screening results often exist only as PDF documents, so text extraction is needed.
Overlapping runs
Section titled “Overlapping runs”Runs can take many hours. Use a single-instance lock so a scheduled run cannot start while the previous one is still going.