Onboarding a new EHR
This playbook is distilled from connecting to more than a dozen EHRs. Follow it in order — each step is cheap, and skipping one is how weeks get lost.
1. Register the app
Section titled “1. Register the app”- Find the vendor’s developer portal (or, for vendors without one, the support or partner channel that enables API access).
- Register your JWKS URL and record the client id, token URL, FHIR base URL and granted scopes.
- Find out whether there is a sandbox. Several vendors offer none, which means every endpoint you test against is production — treat it that way.
2. Prove authentication by hand
Section titled “2. Prove authentication by hand”Get a token, call GET /metadata, then GET /Patient?_count=1. Decode the token and check any
practice or tenant claim: credentials are sometimes issued for a different practice from the one
you asked for. An Organization query settles it in minutes.
3. Check that the practice has actually enabled you
Section titled “3. Check that the practice has actually enabled you”Registering an app is not the same as getting data. Several vendors need a second, per-practice
enablement step. The tell-tale sign: count queries succeed but record reads return HTTP 200 with an
empty searchset. Build a probe that tells “this practice refuses reads” apart from “this practice
genuinely has no data”.
4. Probe what data exists before building anything
Section titled “4. Probe what data exists before building anything”For every resource type your measures need, run GET /<Type>?_count=1 and read total (where the
server reports it honestly — see extraction patterns).
A real example from a specialty practice: about 20,000 patients and 136,000 encounters, but only
about 380 observations — no blood pressures, no A1c, no depression screens — and Procedure.code
always empty. No transport can extract data the practice never charts. Tell stakeholders before
they expect scores.
Then trace one patient end to end, and separate clean zeros from HTTP 400s caused by the shape of your query.
5. Choose the transport
Section titled “5. Choose the transport”- Test
$exportwith a tiny_typefirst (for examplePractitioner). It proves kickoff, status polling, the manifest shape,requiresAccessToken, and whether_typeis honoured. - Don’t trust a 202. One vendor accepted every kickoff, then took 34 minutes to reach 1%, while its status URL dropped into multi-minute 5xx windows.
- If bulk is broken, pull each type with paginated search and stream each page straight to NDJSON on disk. One tenant of about 190,000 resources pulled this way in under two hours.
- Use a per-patient cascade only where forced — some types reject tenant-wide search (“patient or _id must be supplied”), and some servers reject comma-separated patient lists. Scope the cascade to patients with an encounter in the measurement year; in one case that cut 20,700 patients to about 1,000.
6. Normalize before you calculate
Section titled “6. Normalize before you calculate”Assume every new EHR needs a normalization pass. The most common problems:
- References without
type(subject: { reference: "Patient/123" }only). A parser that relies ontypegets empty patient ids — ingest counts look healthy and every measure scores zero. Injecttypefrom the reference prefix. - Encounter status that does not mean what it says — completed visits arriving as
in-progressorunknown. - Attribution missing from the data — TIN is never in FHIR, and NPI is often absent. Plan for a mapping you maintain yourself.
See extraction patterns for the full list.
7. Make the job resilient
Section titled “7. Make the job resilient”- Retry status 0, 429 and 5xx with capped exponential backoff and honour
Retry-After. Fail fast on other 4xx. Tune the retry budget per vendor — some outage windows last ten minutes. - Only failures on the cohort-defining types (Patient, Encounter) should stop the run. Other types should warn and continue, and mark affected patients “incomplete” rather than “no data”.
- Exit non-zero on failure so schedulers notice.
- Archive the raw NDJSON and support “reprocess from archive”, so a change to measure logic never requires another export.
- Log export and status URLs at kickoff so a stuck job can be inspected or resumed.
- Never log full FHIR payloads. They contain PHI.
8. Test with synthetic data
Section titled “8. Test with synthetic data”Use Synthea patients and the vendor’s sandbox where one exists to exercise the pipeline without PHI.