athenahealth
Access
Section titled “Access”- Register an app in athenahealth’s developer portal; it can be listed in their marketplace.
- Authentication is client credentials with a client secret (HTTP Basic), not a signed JWT.
- One app serves every practice. Each request is scoped to a practice with the
ah-practiceparameter, in the forma-1.Practice-<practiceId>:- send it to the token endpoint and as a query parameter on every data call;
- without it, data calls fail with 400 “Could not determine what practice…”;
- the token endpoint is lenient and issues a token even for a malformed value, so validate the format yourself;
- strip any
Organization/prefix.
- A connectivity check: get a token, then
GET /metadata.
Rate limits (observed on one production app)
Section titled “Rate limits (observed on one production app)”- 150 calls per second, 500,000 calls per day, 50 token requests per minute.
- The daily quota resets at midnight GMT and is shared by every practice on the app.
- A per-patient cascade on a 70,000-patient practice (about 7 calls per patient) uses roughly 490,000 calls — a whole day’s allowance — while peaking at only 20% of the per-second limit. Budget for the daily quota, not the burst limit.
Extraction approach
Section titled “Extraction approach”One bulk export for encounters, then a per-patient cascade.
- Kick off
Group/a-1.c-<practiceId>/$exportfor Encounter only. Export latency is unpredictable (observed from 3 minutes to over 4 hours, unrelated to volume), so avoid putting more types through it. - Poll every minute. Treat the export as dead when progress has not changed for an hour (the longest healthy plateau observed was 7 minutes), with an overall ceiling of 24 hours.
_sincefilters on last-updated time, so choose the measurement-year cohort yourself fromEncounter.period.start.- For each cohort patient, search
Condition,Observation,Procedure,DiagnosticReport,DocumentReference,MedicationRequestandServiceRequestbypatient. ReadPatient,PractitionerandMedicationby id.
Why the cascade: population-level search is refused with 403. Patient search needs one of
_id, identifier, name, or family with birthdate, gender or given; Encounter needs
patient or _id.
Known behaviour
Section titled “Known behaviour”Paging
Section titled “Paging”- Always send
_count. Without it you get 20 entries and nonextlink. - Some types cap the page size whatever you ask: Observation and DiagnosticReport 50, ServiceRequest 50, DocumentReference 90, MedicationRequest 100.
- The next-page offset is computed from the
_countyou requested, not from what was served. Ask for 500, receive 50, follownext, and you get 404 “Search has either expired or does not exist”. Rewrite the offset to the number of entries actually received. - Do not re-append
ah-practicetonextURLs that already carry it — a duplicated parameter returns 403 “AND operations are not supported”.
Required parameters
Section titled “Required parameters”MedicationRequestmust haveintent(403 otherwise).intent=order,planworks.- Search
Conditionwithout acategoryto get encounter diagnoses as well as problem-list items.
Data shape
Section titled “Data shape”- 25–35% of completed encounters arrive with status
in-progress. Treat them as finished; leaveplannedalone. - References have no
type; inject it. - Medication codes are on the referenced
Medicationresource, not inline — in one sample, 50 of 50 requests had no inline code. ServiceRequestuses a proprietary order code; only some also carry SNOMED, and those are the referrals.- TIN and practice identifiers are not in FHIR.
Throttling
Section titled “Throttling”Eight parallel workers reliably trigger 429s. Pause the whole cascade on a sustained 429 (start at 60 seconds, double, cap at 15 minutes) and reset the counter as soon as a request succeeds.
Patient matching
Section titled “Patient matching”When matching from a patient list, address-postalcode accepts both ZIP5 and ZIP+4. Patients
without a ZIP cannot be matched reliably.