Skip to content

FHIR access basics

Almost every EHR in this cookbook uses SMART Backend Services for system-to-system access:

  1. You publish a JWKS (your public keys) at a stable URL and register it with the vendor. Most vendors give you back a client id; there is usually no client secret.
  2. To get a token, sign a short-lived JWT (5 minutes is typical) with iss and sub set to your client id, aud set to the token URL, plus jti, iat and exp.
  3. POST it to the token URL as a client_assertion with client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer and grant_type=client_credentials.
  4. Reuse the token until shortly before expires_in (if the server sends none, assume 5 minutes). Refresh once on a 401, and re-mint during long downloads.

Variations you will meet:

Variation Seen on
RS384 signing is the default; ES384 is required ModMed, Meditab
system/*.read rejected — every scope must be listed Oracle Health (Cerner)
Server grants only the intersection of what you ask for and what was provisioned eClinicalWorks, Oracle Health
SMART v2 scope form (system/Observation.rs) ModMed
Token parameter named scopes, not scope NextGen
Client credentials with a client secret instead of a JWT athenahealth, Elation
Client id and secret sent as POST parameters alongside the JWT Amazing Charts

Ask on day one how the vendor separates practices, because it decides where you store credentials:

  • One app, per-practice base URL — Epic (each health system has its own endpoint), ModMed, Practice Fusion, MeldRx.
  • Per-practice client id on a shared host — Nextech, Compulink. The token carries a practice claim.
  • One app plus a per-practice parameter — athenahealth (ah-practice), NextGen (site_id), eClinicalWorks (endpoint code plus group id).
  • Separate registrations for bulk and REST — eClinicalWorks: a bulk client id gets 401 on every REST endpoint.

Kick off an export, poll the status URL, download NDJSON files from the manifest. In theory the best fit for measuring a whole population; in practice it is rarely usable exactly as advertised.

  • _since filters on when a resource was last updated, not on the date of service. Decide measurement-year membership yourself from period.start or effective[x].
  • Never apply _since to reference data such as Practitioner, or older but still-active providers disappear.
  • Check requiresAccessToken in the manifest: some vendors need a bearer token on every file download.

2. Paginated search, one resource type at a time

Section titled “2. Paginated search, one resource type at a time”

Pull each resource type for the whole tenant with ordinary search and follow link.next. Where bulk is broken but population search is allowed, this is often faster and more reliable than bulk.

For each patient in your cohort, search each resource type with patient=<id>. This is the only option on EHRs that forbid population-level search, and the slowest. Keep the cohort small: limit it to patients with an encounter in the measurement year rather than every patient on file.

Most production integrations combine them: one bulk or search pass to find the cohort, then a per-patient cascade for the clinical detail.