Skip to content

ModMed

  • 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/token on 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.

A combination that has worked for five measures in one run:

  1. Bulk Patient/$export without _since — the full roster.
  2. Bulk Encounter/$export with _since=<year>-01-01 — the active cohort and attribution.
  3. Per-patient search for Condition and year-filtered Observation (bulk does not carry everything), plus DocumentReference, MedicationRequest, ServiceRequest and Procedure when a measure needs them.
  4. 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.

  • A 200 can carry an empty or partial output list 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.
  • When bulk never completes, walk the search API in pages of 1,000.
  • _lastUpdated is ignored on Encounter, so apply the year window in your own code.

A bare year (date=2026) returns 200 with total=0. Use prefixed forms: date=ge2026-01-01, or a ge/le pair.

Pages are fixed at 100 entries, and the Bundle total reports the page size, not the number of matches. Always walk next.

  • Bulk Practitioner export returns zero rows, but GET /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.

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.

Runs can take many hours. Use a single-instance lock so a scheduled run cannot start while the previous one is still going.