Resources
Filling Cancelled Appointments in athenahealth
By Federico Ruiz Cassarino, CEO and founder, Puppeteer AI · · 6 min read
athenahealth's scheduling API predates the FHIR era, and it shows: in the URL structure, in the form-encoded bodies, and in a status model that fits in six characters.
It also does two things almost no other EHR in this series does, and both of them matter specifically for filling a cancelled slot.
The status set is six single characters
An athenahealth appointment status is one character. Six values cover the whole set:
o: openf: futurex: cancelled2: checked in3: checked out4: charge entered
Read that list again and notice what's missing. There is no no-show status. f is the documented state for a booked visit, and athenahealth is explicit that it covers appointments that were never checked in even when the date has passed. Their own guidance is that it's up to the practice to cancel an appointment as a no-show when appropriate, which means the missed visit reaches you as a cancellation, or as nothing at all.
This has a direct consequence for outreach: on athenahealth you cannot build a no-show recovery campaign from appointment status alone. The distinction between "told us" and "didn't show" has to come from somewhere else, cancellation records or your own tracking. Any report labeled "no-show rate" sourced purely from status on this system is measuring something else.
The other notable absence is a waitlist state, same as most EHRs in this series. The list lives outside.
You book onto a slot, you don't create an appointment
This is the structural difference from every FHIR-based EHR in this series, and it changes the shape of the whole workflow.
On Canvas or Medplum you POST a new appointment resource into existence. On athenahealth the slot already exists as an object with its own id, and booking is an update to that object:
PUT /v1/{practiceid}/appointments/{appointmentid}
where {appointmentid} is the slot's own id. The body carries patientid, appointmenttypeid, and optionally bookingnote, plus insurance details as a nested insuranceinfo block with insurancecompany, insuranceidnumber, insurancepolicyholder and patientrelationshiptopolicyholder when the visit is billed.
Bodies are form-encoded, not JSON. Booleans go over the wire as strings. If you're used to posting JSON to a FHIR server, this is the first thing that will bite.
So an open slot on athenahealth is a real, addressable object before anyone books it, which means it can also be created, and held.
Creating and freezing slots
Two endpoints exist here that have no counterpart on most systems in this series.
- Creating an open slot.
POST /v1/{practiceid}/appointments/opentakesappointmentdate,appointmenttime,departmentid,provideridandappointmenttypeid. You are adding availability to the schedule programmatically, useful when a cancellation frees time that wasn't previously modeled as bookable. - Freezing a slot.
PUT /v1/{practiceid}/appointments/{appointmentid}/freezewith afreezeflag holds a slot so nobody else can take it.
Freeze is the interesting one. The classic race in any cancellation workflow is that you offer a slot to three patients at once, two accept, and one of them gets an apology. Freezing lets you hold the slot for the duration of the offer and release it if nobody takes it, turning a race into a transaction.
Very few EHRs expose anything equivalent. If you're building outreach that contacts multiple patients about the same opening, this is the feature that makes it safe rather than merely fast.
Change events are polled, not pushed
athenahealth uses a subscription model, but not the webhook kind. You subscribe to appointment changes, then you collect them:
GET /v1/{practiceid}/appointments/changed/subscription/events
Each read drains the queue of changes since the last one. A companion endpoint lets you inspect the subscription itself.
Practically, this means your integration owns the polling loop and its cadence. A cancellation isn't pushed to you the moment it happens; you find out on your next poll. Poll frequently and you burn rate limit. Poll slowly and a slot two days out sits idle while you wait.
The upside over webhooks is real, though: a queue you drain doesn't lose events when your endpoint is down. A missed webhook is gone. An undrained event is still there when you come back.
Cancellation reasons are structured
Cancelling requires a reason id, not free text:
PUT /v1/{practiceid}/appointments/{appointmentid}/cancel
with patientid, appointmentcancelreasonid, and optional cancellationreason notes. The valid ids come from /v1/{practiceid}/appointmentcancelreasons.
This is more than most EHRs in this series give you. Canvas records that an appointment was cancelled and nothing about why. Here the reason is a coded value you can group and report on, and it's the closest thing available to distinguishing a genuine cancellation from an administrative one, which partly compensates for the missing no-show status.
Fetch the reason list at startup and cache it. The ids are practice-specific.
Putting it together: offer, hold, release
The pieces above compose into a workflow that's genuinely different from what you'd build on a FHIR EHR.
- Drain the change queue. A cancellation appears as an event; the slot's status returns to
o. - Freeze the slot. Before contacting anyone, hold it. Now the race is closed. Staff booking through the UI won't take it out from under an offer in flight.
- Offer it. To one patient, or to several with a short window, since the slot can't be double-booked while frozen.
- Book or release. Whoever accepts gets a
PUTonto the slot id. If nobody accepts inside the window, unfreeze and let the slot return to normal availability.
Step four's release is the one that gets skipped, and skipping it is worse than never freezing at all. A frozen slot nobody released is a slot no patient and no staff member can book: availability that has silently disappeared from the schedule with no visible cause.
Whatever freezes must be able to unfreeze, including after a crash. A timeout that releases holds older than your offer window is not optional. It's the difference between a safe workflow and a slow leak in the practice's calendar.
What you need on your side
- Credentials and a practice id. Every path is scoped by practice, and it's in the URL, not a header.
- Department ids. Slot searches are scoped by
departmentid, the organizing unit rather than location or provider. Provider and appointment type narrow further, optionally. - A polling loop. For the change subscription, with a cadence you've thought about.
- The cancellation reason list. Cached, and mapped to whatever your own reporting uses.
Honest limits
- No no-show status. The single biggest gap. Missed-visit reporting and recovery outreach both need a source outside appointment status.
- Polling latency is your floor. However fast your outreach is, it starts at the next poll. Sub-minute reaction time isn't available at reasonable request volumes.
- Form-encoded, non-JSON bodies. Across the whole API. It works, it's just a different client than the rest of your integrations, and boolean handling is a recurring source of bugs.
- Practice-specific ids everywhere. Cancellation reasons, appointment types, and departments are all scoped per practice. Nothing is portable between them, so a multi-practice deployment carries a mapping layer.
- Primary docs are gated. athenahealth's own API documentation sits behind a developer account and returns 403 to the open web. The status codes above come from a public client library that quotes the field docs verbatim; confirm them against the portal once you have credentials.
Sources
About the author
Federico Ruiz Cassarino is the CEO and founder of Puppeteer AI, the company behind Recupra. He works with clinic operators on the scheduling and outreach problems that decide how much of a practice's booked capacity turns into revenue. Connect on LinkedIn.