Skip to content

athenahealth

  • 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-practice parameter, in the form a-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.

One bulk export for encounters, then a per-patient cascade.

  1. Kick off Group/a-1.c-<practiceId>/$export for Encounter only. Export latency is unpredictable (observed from 3 minutes to over 4 hours, unrelated to volume), so avoid putting more types through it.
  2. 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.
  3. _since filters on last-updated time, so choose the measurement-year cohort yourself from Encounter.period.start.
  4. For each cohort patient, search Condition, Observation, Procedure, DiagnosticReport, DocumentReference, MedicationRequest and ServiceRequest by patient. Read Patient, Practitioner and Medication by 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.

  • Always send _count. Without it you get 20 entries and no next link.
  • 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 _count you requested, not from what was served. Ask for 500, receive 50, follow next, 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-practice to next URLs that already carry it — a duplicated parameter returns 403 “AND operations are not supported”.
  • MedicationRequest must have intent (403 otherwise). intent=order,plan works.
  • Search Condition without a category to get encounter diagnoses as well as problem-list items.
  • 25–35% of completed encounters arrive with status in-progress. Treat them as finished; leave planned alone.
  • References have no type; inject it.
  • Medication codes are on the referenced Medication resource, not inline — in one sample, 50 of 50 requests had no inline code.
  • ServiceRequest uses a proprietary order code; only some also carry SNOMED, and those are the referrals.
  • TIN and practice identifiers are not in FHIR.

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.

When matching from a patient list, address-postalcode accepts both ZIP5 and ZIP+4. Patients without a ZIP cannot be matched reliably.