Integrations

FHIR Bulk Data Export: Using $export Against Your EMR

FHIR bulk data export is the standardized way to pull many patients' records out of your EMR at once — an asynchronous job you kick off with an $export operation, poll until it finishes, and then download as newline-delimited JSON files. It exists because the ordinary FHIR API is built for one patient at a time, and looping it across 20,000 patients is a denial-of-service attack on your own EMR. If your EMR is certified to 45 CFR 170.315(g)(10) — "Standardized API for patient and population services" — it has to support this: ONC's test method for the criterion requires the HL7 FHIR Bulk Data Access (Flat FHIR) implementation specification, including mandatory support for the group-export operation.

What FHIR bulk export is

Three ideas, and once you have them the rest is plumbing:

  • It is asynchronous. The first request returns a job, not data. The server works in the background; you poll a status URL until it hands you a manifest.
  • The output is ndjson. Newline-delimited JSON — one resource per line, one or more files per resource type. Not a FHIR Bundle. It streams, it splits, and it never requires the whole payload in memory.
  • It is population-scale. The authorization model differs too: no user is sitting there clicking approve, so it uses system-level backend authentication rather than the interactive SMART app launch a patient-facing app would use.

Typical uses: quality-measure and analytics platforms, registry or research cohorts, data-warehouse loads, payer and ACO reporting.

What a certified EMR must support

The certification criterion is your leverage in a vendor conversation. Per ONC's test method for §170.315(g)(10), a certified Health IT Module supports:

ElementWhat the criterion requires
Base FHIR standardHL7 FHIR Release 4 (R4)
Data profilesHL7 FHIR US Core Implementation Guide, aligned to the adopted USCDI version
Bulk exportFHIR Bulk Data Access (Flat FHIR), STU 1, including mandatory support for the "group-export" OperationDefinition
Authorization (system scopes)The SMART Backend Services: Authorization Guide section of the adopted implementation specification; the app must be issued a valid access token, and the granted scope must be no greater than the pre-authorized scope
Privacy and securityThe certificate must include the §170.315(d) privacy and security criteria within its scope

Two things follow. First, newer versions — notably Bulk Data Access STU 2 and later US Core releases — are available through ONC's Standards Version Advancement Process (SVAP), so your vendor may be ahead of the baseline. Ask which, in writing. Second, if a vendor tells you bulk export is a paid add-on, check their listing on the Certified Health IT Product List (CHPL) first. A product certified to (g)(10) has attested to this capability.

The three export endpoints

LevelRequestScope of the export
GroupGET [base]/Group/[id]/$exportAll members of a Group resource defined in the EMR. This is the one certification mandates.
PatientGET [base]/Patient/$exportAll patients the authorized client is permitted to see
SystemGET [base]/$exportEverything in the server's scope, including resources not linked to a patient

Plan your integration around the Group endpoint. It is the one you can rely on being there, and it is the right shape for the work anyway: you want a cohort, not "every patient in the database." The catch is that the Group has to exist in the EMR — somebody has to build and maintain it. That is a workflow problem, not a technical one, and it is the step that quietly delays these projects.

The export flow, step by step

  1. Get a token. Authenticate using the SMART Backend Services flow (below), requesting system-level scopes for the resource types you need.
  2. Kick off the job. Call the export endpoint with Accept: application/fhir+json and Prefer: respond-async. Narrow it while you are here: _type limits resource types, _since pulls only what changed after a timestamp. A daily incremental with _since is dramatically cheaper than a nightly full export.
  3. Take the status URL. A successful kick-off returns 202 Accepted with a Content-Location header. That URL is the job.
  4. Poll politely. GET the status URL. While the job runs you get 202, often with Retry-After and an X-Progress hint. Honor Retry-After; polling in a tight loop is how integrations get throttled.
  5. Read the manifest. On completion you get 200 OK and a JSON manifest with a transactionTime, the request URL, and an output array of file URLs tagged by resource type. Check the error array too — it holds OperationOutcome resources for rows the server could not produce.
  6. Download the files. Fetch each ndjson file with your access token. Expect several files per resource type on large exports.
  7. Clean up. Send DELETE to the status URL when you are done. Files expire anyway, but a well-behaved client tells the server to stop holding them.

Authorization: SMART Backend Services

This is where most first attempts stall, because it is not the OAuth flow anyone remembers. No user, no login screen, no redirect — the client authenticates as a system:

  • You register your client with the EMR's authorization server and provide a public key (or a JWKS URL where it can be fetched).
  • To get a token, your client builds a JWT assertion, signs it with the matching private key, and posts it to the token endpoint as a client credentials grant.
  • The server returns a short-lived access token carrying system scopessystem/Patient.read, system/Observation.read, and so on.
  • ONC's test method requires that the scope granted is no greater than the pre-authorized scope registered for that client. If your token comes back thinner than you asked for, the problem is usually your registration, not your code.
Operationally, the private key is the whole ballgame. It can read your entire patient population with no human in the loop. Store it in a secrets manager or HSM — not in the repo, not in an environment variable on a shared box. Rotate it, and revoke it the day the vendor relationship or the engineer's employment ends. Registration and key exchange with the vendor usually take longer than writing the client, so start that paperwork first.

What bulk export will not give you

The most common disappointment: (g)(10) bulk export returns the data classes and elements defined by USCDI, shaped by the US Core profiles. That is a standardized, genuinely useful slice of the chart — and it is not the whole chart. Scanned documents, free-text notes outside the supported profiles, images, vendor-proprietary fields and configuration data are generally not in scope.

If you need everything — you are migrating EMRs, or you need the complete legal record — the capability you want is Electronic Health Information (EHI) export under §170.315(b)(10), a different criterion with a different purpose. Bulk export is for populations and analytics; EHI export is for wholesale extraction.

Practical gotchas

  • Run it against a sandbox first. A full-population export against production, mid-clinic, is a disruptive amount of I/O. Run production exports in the maintenance window.
  • Expect the job to fail partway. Long jobs die. Build retry and resume logic, and treat a partial manifest as normal.
  • Read the error array. Populated output plus a populated error array is a partial success. Clients that ignore it silently ship incomplete datasets into analytics.
  • ndjson is not JSON. Do not parse the file as a single document — stream it line by line, or you will exhaust memory on the first real export. Output files also expire, so do not let the download step lag hours behind the job.
  • It is still ePHI. The moment those files land on disk they are ePHI outside the EMR's access controls. Encrypt them at rest, restrict and log access to the landing directory, and delete them on a schedule.

Common questions

Does my EMR support FHIR bulk export?

If it is certified to 45 CFR 170.315(g)(10), it is required to. ONC's test method for that criterion requires the FHIR Bulk Data Access (Flat FHIR) implementation specification including the group-export operation. Verify the certification on the Certified Health IT Product List (CHPL) rather than relying on the sales team.

What is the difference between $export and EHI export?

Bulk $export under (g)(10) returns USCDI data shaped by US Core FHIR profiles — a standardized subset built for population analytics. EHI export under §170.315(b)(10) is a separate criterion aimed at extracting the electronic health information a system holds, which is what you need when migrating systems.

Do I need a user login for a bulk export?

No, and that is the point. Bulk export uses the SMART Backend Services flow: the client signs a JWT assertion with a private key, exchanges it for an access token with system-level scopes, and runs with no interactive user. Guard that private key — it can read your whole population.

How often should we run a full export?

Rarely. Use _since for incrementals and reserve full exports for the initial load and periodic reconciliation. Full population exports are expensive for the EMR and expensive for you to store securely.

Common questions

Does my EMR support FHIR bulk export?

If it is certified to 45 CFR 170.315(g)(10), Standardized API for patient and population services, it is required to. ONC's test method for that criterion requires the FHIR Bulk Data Access (Flat FHIR) implementation specification including mandatory support for the group-export operation. Verify the product's certification on the Certified Health IT Product List (CHPL).

What is the difference between $export and EHI export?

Bulk $export under (g)(10) returns USCDI data shaped by US Core FHIR profiles — a standardized subset built for population analytics. Electronic Health Information (EHI) export under 45 CFR 170.315(b)(10) is a separate certification criterion aimed at extracting the electronic health information a system holds, which is what you need when migrating systems.

Do I need a user login to run a bulk export?

No. Bulk export uses the SMART Backend Services flow: your client signs a JWT assertion with a private key, exchanges it for a short-lived access token carrying system scopes, and runs with no interactive user. That private key can read your entire patient population, so store it in a secrets manager and rotate it.

Why is the output not a FHIR Bundle?

Bulk export returns newline-delimited JSON (ndjson) — one resource per line, one or more files per resource type. That format streams and splits, so neither the server nor your client has to hold the whole payload in memory. Parse it line by line rather than as a single JSON document.