Nirmitee.io
FHIREHR IntegrationAI Agents

FHIR Scheduling API: How to Book Appointments with Slot and Appointment in Epic, Oracle Health and athenahealth (2026)

September 6, 202622 min readUpdated Sep 25, 2026
Written by
Jitendra Choudhary
Jitendra Choudhary

CTO & Co-Founder

CTO & Co-Founder at Nirmitee.io. Architects healthcare integrations across FHIR, SMART on FHIR, ABDM and NHCX, writing from production experience taking hospital software from sandbox to go-live.

FHIR Scheduling API: How to Book Appointments with Slot and Appointment in Epic, Oracle Health and athenahealth (2026)

A FHIR scheduling API lets an app find open time with the Slot resource and book it with the Appointment resource, so a patient, a front-desk tool or a voice agent can put a visit on a real EHR calendar. The catch is that Epic, Oracle Health and athenahealth each expose a different slice of that model, and the booking call that works in one EHR does not exist in the next.

This guide is for teams building patient access products and AI voice schedulers that must book into the EHR without double-booking. It covers the four FHIR R4 scheduling resources, the booking flow, what each vendor documents today, and the sync and agent-safety work that decides whether you ship. Vendor claims link to vendor documentation checked in September 2026.

Key takeaways

  • FHIR R4 models scheduling with four resources: Schedule holds Slots, an Appointment fills one or more Slots, and participants reply with AppointmentResponse. All four are Trial Use at maturity level 3.
  • Oracle Health Millennium is the closest to plain REST: search free Slots, then POST an Appointment with status: booked and exactly one slot reference.
  • Epic books through STU3 Appointment/$find and $book, which only work after the health system builds scheduling rules in Cadence. Its R4 Appointment search requires a patient.
  • athenahealth's public FHIR R4 CapabilityStatement lists Appointment read and search only, with no Slot and no create, so booking runs through its proprietary athenaOne APIs: GET /appointments/open to find slots and PUT /appointments/{appointmentid} to book one.
  • None of the three public R4 CapabilityStatements lists _lastUpdated for Appointment or a Subscription resource. athenahealth documents Subscription outside its certified CapabilityStatement, as Event Notifications webhooks; elsewhere, plan sync around date-window polling or HL7 v2 SIU feeds.
  • For voice and AI agents, identity verification, read-back confirmation and human handoff belong in your application, not in the EHR call.

What are the FHIR scheduling resources and how do they relate?

FHIR R4 splits scheduling into four resources. Schedule is the container for a practitioner, location or service. Slot is a bookable interval on that Schedule with a free or busy status. Appointment is the booking that fills one or more Slots. AppointmentResponse records whether each participant accepts.

The spec is explicit that a Schedule "does not provide any information about actual appointments." It only frames the time. Availability lives on Slot, and the booking lives on Appointment. The table below lists the elements you will touch most, with cardinalities from the R4 Slot, R4 Appointment, R4 Schedule and R4 AppointmentResponse pages.

ResourceWhat it representsKey elements (R4 cardinality)Search parameters defined in R4
ScheduleThe calendar container for one actor: a Practitioner, Location, HealthcareService or Deviceactor 1..*, planningHorizon 0..1, active 0..1, serviceType 0..*active, actor, date, identifier, service-category, service-type, specialty
SlotOne bookable interval on a Scheduleschedule 1..1, status 1..1 (free, busy, busy-unavailable, busy-tentative, entered-in-error), start 1..1, end 1..1, overbooked 0..1, appointmentType 0..1appointment-type, identifier, schedule, service-category, service-type, specialty, start, status
AppointmentThe booking itselfstatus 1..1, slot 0..*, participant 1..* (each with status 1..1), start/end 0..1, requestedPeriod 0..*, serviceType 0..*actor, appointment-type, based-on, date, identifier, location, part-status, patient, practitioner, reason-code, reason-reference, service-category, service-type, slot, specialty, status, supporting-info
AppointmentResponseOne participant's reply to a proposed appointmentappointment 1..1, participantStatus 1..1 (accepted, declined, tentative, needs-action), actor 0..1actor, appointment, identifier, location, part-status, patient, practitioner

Two Appointment invariants matter for booking code. Start and end must both be present or both absent, and only proposed or cancelled appointments may omit them. So a booked appointment always carries a concrete time, while a request for "sometime next week" is a proposed appointment with a requestedPeriod.

How does FHIR appointment booking work, step by step?

A FHIR appointment booking follows six steps: identify the patient, search availability, optionally hold the time, book, confirm what the EHR stored, and handle the response. The resources are standard, but each step maps to a different call per vendor, and skipping the confirmation read is the most common source of silent failures.

  1. Identify the patient with a two-step search. Search Patient with strong demographics, then read the single match to confirm it. Never book against a fuzzy or multiple match. See our FHIR Patient resource guide and patient matching beyond demographics.
  2. Find availability. Search Slot by service type, location or practitioner, or call an operation such as Appointment/$find. Always bound the window with a lower and upper date.
  3. Hold the time if supported. The Argonaut IG defines $hold, and Epic holds $find results temporarily. Oracle Health documents no hold operation. Its Slot patch can set status to busy-tentative or back to free under Provider or System authorization, but Oracle does not describe that as a hold, so treat the slot as yours only once the create succeeds.
  4. Book. POST an Appointment that references the Slot with status: booked, or call $book with the proposed appointment ID.
  5. Confirm. Read the created Appointment back from the Location header or the operation response. Check status, start, end, practitioner and location against what the patient agreed to.
  6. Handle the response. Store the Appointment ID and version (ETag) for later cancel or reschedule. On failure, re-search availability rather than retrying the same slot blindly.

Here is a real Slot search against Oracle Health's open R4 sandbox, captured on 12 September 2026 and trimmed to one entry. Note the required pairing of ge and lt on start, and the next link for paging.

GET https://fhir-open.cerner.com/r4/ec2458f2-1e24-41c8-b71b-0e701af7583d/Slot?service-type=https://fhir.cerner.com/ec2458f2-1e24-41c8-b71b-0e701af7583d/codeSet/14249|4047611&-location=32216049&start=ge2026-09-14T00:00:00Z&start=lt2026-10-14T00:00:00Z&_count=3

{
  "resourceType": "Bundle",
  "type": "searchset",
  "link": [
    { "relation": "next", "url": "https://fhir-open.cerner.com/r4/ec2458f2-1e24-41c8-b71b-0e701af7583d/Slot?-pageContext=4f5594d0-7d8a-4753-a68c-2cc9f09c1091&-pageDirection=NEXT" }
  ],
  "entry": [
    {
      "fullUrl": "https://fhir-open.cerner.com/r4/ec2458f2-1e24-41c8-b71b-0e701af7583d/Slot/4047611-32216049-67336475-1140",
      "resource": {
        "resourceType": "Slot",
        "id": "4047611-32216049-67336475-1140",
        "extension": [
          {
            "url": "https://fhir-ehr.cerner.com/r4/StructureDefinition/scheduling-location",
            "valueReference": { "reference": "Location/32216049" }
          }
        ],
        "serviceType": [
          {
            "coding": [
              { "system": "https://fhir.cerner.com/ec2458f2-1e24-41c8-b71b-0e701af7583d/codeSet/14249", "code": "4047611", "display": "Surgery Rapid" }
            ],
            "text": "Surgery Rapid"
          }
        ],
        "schedule": { "reference": "Schedule/4047611-32216049-67336475-1140" },
        "status": "free",
        "start": "2026-09-14T00:00:00Z",
        "end": "2026-09-14T01:00:00Z"
      }
    }
  ]
}

And here is the booking request, taken from Oracle Health's Appointment create documentation. A booked appointment needs exactly one slot reference and a patient participant with status accepted. The server answers 201 Created with a Location header and an ETag.

POST [base]/Appointment
Content-Type: application/fhir+json

{
  "resourceType": "Appointment",
  "status": "booked",
  "slot": [
    { "reference": "Slot/24477854-21304876-62852027-0" }
  ],
  "participant": [
    {
      "actor": { "reference": "Patient/12724066" },
      "status": "accepted"
    }
  ],
  "reasonCode": [
    { "text": "I have a cramp" }
  ]
}

What does the Argonaut Scheduling IG define: $find, $hold and $book?

The Argonaut Scheduling Implementation Guide defines four operations: Slot/$prefetch, Appointment/$find, Appointment/$hold and Appointment/$book. Its only formal release is 1.0.0, published April 2018 on FHIR STU3 (3.0.1). That STU3 base is why Epic's scheduling operations still carry STU3 URLs.

The Argonaut operations page describes two patterns. In real time, the client calls $find with a required start and end plus optional specialty, visit type, practitioner, location and patient, gets proposed Appointments back, and books one by ID. In prefetch, the client pulls free Slots with $prefetch, builds its own Appointment, and books by passing the resource.

Three details are worth copying even if your EHR does not implement Argonaut:

  • Holds expire on the server's terms. The IG says the hold length "is determined by the scheduling service's business rules" and that a successful hold should include an Expires header. Build a countdown, not an assumption.
  • Rescheduling is cancel plus rebook. $book takes an optional cancelled-appt-id, so a reschedule is one call that books the new time and releases the old one.
  • The availability answer is a proposal. $find returns proposed Appointments, and a successful $hold moves one to pending (or cancelled if the hold is rejected). Your UI should never show a proposed or held time as confirmed.

What Epic, Oracle Health and athenahealth support for FHIR scheduling

Each EHR takes a different path. Epic uses STU3 operations gated by customer build, Oracle Health exposes R4 Slot search plus Appointment create and patch, and athenahealth reads appointments in FHIR but books through its proprietary API. For the athenahealth side from sandbox access to production, see our guide on how to integrate with athenahealth.

CapabilityEpicOracle Health (Millennium R4)athenahealth
Availability searchPOST Appointment/$find (STU3). Slot is read-only and "accessible only from the Appointment $find resource"GET Slot with service-type, -location or schedule.actor, plus a start range using both ge and lt. Returns free slots onlyNo Slot or Schedule in FHIR R4. Proprietary GET /v1/{practiceid}/appointments/open with departmentid and a reasonid or appointmenttypeid
Booking methodPOST Appointment/$book (STU3) with patient ID, appointment ID from $find and an optional notePOST Appointment with status: booked, one slot, patient participant accepted. Or status: proposed with a requestedPeriodNo FHIR create. Proprietary PUT /v1/{practiceid}/appointments/{appointmentid} on an open slot, with patientid and reasonid
Cancel and rescheduleNo FHIR cancel or reschedule. The Epic on FHIR catalog lists only Read and Search APIs plus STU3 $find and $book for Appointment, and $book takes no cancelled-appointment input. Epic's Incoming Appointment Scheduling HL7v2 interface accepts new, rescheduled, updated and canceled appointmentsJSON Patch: replace /status with cancelled plus /cancelationReason. Reschedule by replacing /slot with a reschedule-reason extension. If-Match requiredProprietary PUT .../appointments/{appointmentid}/cancel and PUT .../appointments/{appointmentid}/reschedule with newappointmentid
Patient required on Appointment searchYes. R4 Appointment.Search marks patient as requiredOne of patient, practitioner or location (never combined), or _id; with date or -date-or-req-periodCapabilityStatement lists _id, ah-practice, _query and group appointment IDs, no patient parameter
Who can book$find and $book list backend systems and clinician or administrative users, not patientsCreate and patch support Provider and System authorization, not PatientApps approved for the athenaOne appointment APIs, which need a Platform Services contract and Solution Validation for production
NotesRequires pre-coordination: the organization must build rules in Cadence. $find pages 10 results by default and does not return existing appointmentsPatient-authorized Appointment search does not return sensitive appointmentsFHIR Appointment adds $self-checkin-entry-url and $telehealth-zoom-credentials operations. Open-slot search defaults to a seven-day window and 1,000 results per page

Epic: $find and $book, built per health system

Epic's Appointment.$find (STU3) specification opens with a warning that shapes the whole project: the operation "requires pre-coordination with the healthcare organization," which must build rules in its Cadence scheduling system that decide which providers and slots come back. The listed inputs (start and end time, a Patient resource with demographics and a time zone extension, service type as the visit type, specialties, location, indications, referrals and time of day) are potential elements. Each organization may ignore or require any of them, though the specification marks service-type (the visit type) as 1..1.

$find returns no overlapping slots, pages 10 results at a time, holds proposed slots only temporarily and never returns existing appointments. Appointment.$book (STU3) must follow it. It takes the patient ID, the appointment ID from $find and an optional note, and returns an Appointment whose status is always booked and whose identifier is the CSN. Epic's sample request:

POST [base]/api/FHIR/STU3/Appointment/$book
Content-Type: application/fhir+json

{
  "resourceType": "Parameters",
  "parameter": [
    { "name": "patient", "valueIdentifier": { "value": "efvHwbc1k1CQ9XjM1zvvefQ3" } },
    { "name": "appointment", "valueIdentifier": { "value": "ezu6MfS.FpOXrHAn1eJHczv4LlH.fMIwtdkA8rsm-Yfu96eUh91EBd0UN9BZx7kbB3" } },
    { "name": "appointmentNote", "valueString": "Note text containing info related to the appointment." }
  ]
}

In R4, Epic's sandbox CapabilityStatement (August 2026) lists Appointment read and search only, with no Slot or Schedule. The R4 Appointment.Search specification requires patient, searches natively on date, status, service-category and identifier (the encounter CSN), applies other parameters such as practitioner and location as post-filters, marks slot unsupported, and excludes scheduled surgeries and Book Anywhere appointments at other organizations. R4 tells you what a known patient has booked. It does not find open time. For registration and sandbox setup, see our Epic FHIR integration guide.

Oracle Health: REST Slot and Appointment in R4

Oracle Health's Slot search accepts _id alone, or a service type, location or practitioner with a bounded start range, and returns only free slots sorted by start. Besides booked, create accepts proposed, which needs one serviceType, a patient and a location participant set to needs-action, and one requestedPeriod. For booked, participant.type must not be set, and reasonCode, if sent, holds exactly one CodeableConcept.

Two search details matter for a booking UI. Slot service-type values must be all standard codes or all Millennium codes, and on the secured endpoint _include=Slot:schedule or _include=Schedule:actor:Practitioner needs the matching Schedule (and Practitioner) read scopes on top of the Slot scope. On Appointment search, date skips proposed appointments, while -date-or-req-period also returns proposed ones whose requested period falls in the range.

Changes use JSON Patch. The Appointment patch documentation supports status changes to arrived, checked-in, cancelled, booked and fulfilled, and returns 412 Precondition Failed without If-Match, 409 Conflict for a stale version, 423 Locked while another update is in progress, and 422 for a participant status other than accepted. Setting status to booked must come with an add on /slot. A cancel:

PATCH [base]/Appointment/20465903
Content-Type: application/json-patch+json
If-Match: W/"0"

[
  { "op": "replace", "path": "/status", "value": "cancelled" },
  {
    "op": "add",
    "path": "/cancelationReason",
    "value": {
      "coding": [
        { "system": "https://terminology.hl7.org/CodeSystem-appointment-cancellation-reason.html", "code": "oth-err" }
      ]
    }
  }
]

athenahealth: FHIR for reading, proprietary API for booking

athenahealth's live FHIR R4 CapabilityStatement at api.platform.athenahealth.com/fhir/r4/metadata lists Appointment with read and search-type interactions, and no Slot, Schedule or create. Its documented Appointment operations are a patient self check-in link (valid for up to a week or 10 uses, per the CapabilityStatement) and telehealth Zoom credentials. Open slots, booking, cancellation and rescheduling all live in the athenaOne APIs, covered below. Our athenahealth API developer guide walks through credentials, practice IDs and the split between the two API families.

athenahealth's Appointment Slot reference defines GET /v1/{practiceid}/appointments/open. departmentid is required, and a request needs either a reasonid or an appointmenttypeid, or it returns nothing. The reference steers you to reasonid, a patient appointment reason from GET /patientappointmentreasons that maps to the right appointment type and follows the practice's web scheduling setup; appointmenttypeid ignores that setup, and athenahealth asks you to consult it before using it. Any reasonid other than -1 also requires providerid. The special value -1 returns open, web-schedulable slots for any reason, but some of those slots cannot be booked by any reason ID, and the reschedule reference adds that -1 availability ignores each reason's minimum lead time. Enumerate the reasons you want instead (reasonid=1,2,3). Dates use mm/dd/yyyy, the window defaults to seven days from startdate, and practice rules hide slots inside a minimum lead time or beyond a maximum horizon (24 hours and 90 days by default) unless you pass bypassscheduletimechecks.

The requests below follow the parameter tables in athenahealth's references; the IDs and dates are placeholders against the shared preview practice.

# 1. Open slots for one department, reason and provider
curl -s -G "https://api.preview.platform.athenahealth.com/v1/195900/appointments/open" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  --data-urlencode "departmentid=$DEPARTMENT_ID" \
  --data-urlencode "reasonid=$REASON_ID" \
  --data-urlencode "providerid=$PROVIDER_ID" \
  --data-urlencode "startdate=09/21/2026" \
  --data-urlencode "enddate=09/28/2026"

# 2. Book the chosen slot
curl -s -X PUT "https://api.preview.platform.athenahealth.com/v1/195900/appointments/$APPOINTMENT_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d "patientid=$PATIENT_ID" \
  -d "reasonid=$REASON_ID" \
  -d "departmentid=$DEPARTMENT_ID"

# 3. Cancel, or move the visit to another open slot
curl -s -X PUT "https://api.preview.platform.athenahealth.com/v1/195900/appointments/$APPOINTMENT_ID/cancel" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d "patientid=$PATIENT_ID" \
  -d "cancellationreason=Patient request"

curl -s -X PUT "https://api.preview.platform.athenahealth.com/v1/195900/appointments/$APPOINTMENT_ID/reschedule" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d "patientid=$PATIENT_ID" \
  -d "newappointmentid=$NEW_APPOINTMENT_ID"

Booking is a PUT on the open slot's appointmentid, per the Appointment reference. patientid is required, reasonid is required for web-based scheduling, and appointmenttypeid is reserved for digital check-in kiosks and apps used by practice staff. Cancel takes patientid plus an optional appointmentcancelreasonid or free-text cancellationreason, and the appointment status generally comes back as x. Reschedule takes newappointmentid and patientid, keeps the original reason unless you pass one, and since release 26.01.22 also works on cancelled appointments, per athenahealth's release notes. To change a booked visit's department, provider or appointment type without moving it, use PUT /appointments/booked/{appointmentid}. None of this has a FHIR equivalent at athenahealth today: the R4 CapabilityStatement lists Appointment read and search only, with no Slot or Schedule.

Scoping a scheduling product across more than one EHR? We build and run these integrations for product teams, including the per-site build conversations Epic requires. Talk to our team and we will map what booking actually looks like in each of your target EHRs before you write the adapter. For the wider API picture beyond scheduling, see our EHR integration API comparison.

Why does FHIR slot availability differ from one health system to the next?

Availability is not a property of the API. It is the output of scheduling rules each health system configures: visit types, provider templates, departments, and the decision logic that maps a patient's reason to a bookable slot. Two sites on the same EHR version can return different answers for the same query, and neither is wrong.

Epic says $find results come from "organization-defined rules." Oracle Health's Slot search keys on a service type from the site's own code set (codeSet/14249 in the sandbox), so the codes you send are local configuration. The factors that change the answer:

  • Visit types. A new-patient visit, a follow-up and a telehealth visit are separate bookable types with different lengths. Sending the wrong one returns nothing or the wrong slots.
  • Provider templates. Each provider's template decides which hours accept which visit types. A slot can be free on the calendar and still not bookable for your visit type.
  • Departments and locations. The same provider may book differently at two clinics. Oracle Health's Slot carries a scheduling-location extension, so the location is part of the availability answer.
  • Decision trees and referral rules. Epic's $find accepts indications and referrals because sites route patients to the right visit type and provider using them.
  • Patient-facing booking permissions. Sites decide which visit types patients or third parties may self-schedule. Many keep complex visits staff-only.

Budget a discovery session per health system to collect visit type codes, departments and booking rules, and store them as per-tenant configuration. A site's FHIR CapabilityStatement tells you which calls exist, not which slots it will offer.

How do you keep FHIR appointments in sync with the EHR?

You have three options: poll the FHIR Appointment search, consume an HL7 v2 SIU feed from the EHR, or subscribe to change notifications where offered. For the three EHRs here, polling is limited to date windows, subscriptions are not in their public R4 CapabilityStatements, and SIU is the event feed most health systems can already send.

ApproachHow it worksWhat limits it in practiceUse it when
Polling FHIR Appointment searchRepeat searches by patient, practitioner or location over a date window and diff the resultsR4 defines _lastUpdated, but none of the three CapabilityStatements lists it for Appointment. Epic requires patient, so polling scales per patient. Rate limits applyLow volume, patient-centric apps, or confirming a single booking
HL7 v2 SIU feedThe EHR sends SIU messages for scheduling events to your interface engineNeeds an interface build with each health system and an engine to receive, parse and map messagesProvider or department-wide calendars, reminders, waitlists and no-show workflows
FHIR SubscriptionThe server notifies you when matching resources changeNot listed in the three public R4 CapabilityStatements. athenahealth documents a separate, non-certified Subscription API (R5 Backport based, rest-hook, id-only) with Appointment topics. The Subscriptions R5 Backport IG is STU for R4 serversWhere a vendor documents support, such as athenahealth Event Notifications

HL7 v2 defines the SIU events you will map. From the HL7 v2 event type table: S12 new appointment booking, S13 rescheduling, S14 modification, S15 cancellation, S17 deletion and S26 patient no-show. Epic's Outgoing Appointment Scheduling interface sends messages for new, rescheduled, updated, no-showed and canceled appointments. We run Mirth Connect engines in production for feeds like this, and HL7 v2 stays the practical choice when a site cannot expose change events in FHIR.

A common pattern: book through FHIR or the vendor API, and treat the SIU feed as the source of truth for staff changes. athenahealth documents two change channels for appointments: changed data subscriptions, a polling feed of schedule, cancel, check-in, check-out, freeze and new-slot events, and Event Notifications, id-only webhooks on topics such as Appointment.schedule, Appointment.reschedule and Appointment.cancel for 2-legged apps with an updated API Solutions contract. For the event-driven architecture behind this, see our post on FHIR subscriptions and real-time clinical pipelines.

How do you prevent double-booking? Idempotency and race conditions

Double-booking happens in the gap between "this slot is free" and "the create succeeded." Treat every availability answer as stale, make every booking attempt safe to retry, and let the EHR decide who won. Never mark a slot taken in your own database before the EHR confirms it.

  • Stale slot. A patient picks a time fetched two minutes ago, and staff booked it in the meantime. A free Slot is only free at read time. On a failed create, re-search and offer new options.
  • Retry after timeout. The booking POST times out, your code retries, and the patient gets two appointments. Before retrying, search the patient's appointments for that date to see whether the first attempt landed.
  • Concurrent edits. Two workflows reschedule the same appointment. Oracle rejects a patch without If-Match with 412 and a stale version with 409. Store the ETag from every create and read.
  • Expired hold. An Epic $find result is held only temporarily, and an Argonaut hold ends when the server's Expires time passes. A voice call that takes four minutes to collect insurance details can outlive it.

The R4 RESTful API defines conditional create with If-None-Exist, but none of these three EHRs advertises it for Appointment. Build idempotency in your own layer: a client-generated key per booking attempt, persisted before the EHR call and checked before any retry.

Building a voice or AI scheduling agent on the FHIR scheduling API

A voice or AI scheduling agent uses the same booking calls as any app, but acts on spoken, error-prone input with no screen, and the EHR cannot tell whether the caller is the patient. Identity verification, read-back confirmation and human handoff must be built into the agent, which books only after they pass.

Booking makes the agent a writer, not a reader (see the FHIR write problem for AI agents). The design rules we apply:

  • Verify identity in the app, before any lookup that reveals data. Collect name, date of birth and a second factor such as a callback number or one-time code, then run the two-step Patient search. If the search returns zero or several matches, do not guess. Hand off.
  • Constrain the request to bookable visit types. Map the caller's reason to a short list of visit types the site allows for self-scheduling. Everything else goes to staff.
  • Offer few options, then read back. Present two or three times. Before booking, read back provider, location, date, time and time zone, and require an explicit yes. Epic's $find accepts a patient time zone extension, which matters for telehealth and callers in other states.
  • Book, then confirm from the EHR. Speak the confirmation only after reading the created Appointment back. Send a text or email with the same details from that record.
  • Hand off with context. Transfer to a human on failed verification, no availability, a failed booking, a clinical symptom that needs triage, or any request to cancel on someone else's behalf. Pass the collected details so the caller does not repeat themselves.
  • Log every tool call. Keep the request, response and transcript turn for each search and booking under your HIPAA audit controls.

For the surrounding orchestration, see AI-powered scheduling orchestration and a hospital scheduling agent workflow.

Where teams get stuck with FHIR scheduling integration

Scheduling projects rarely slip on FHIR syntax. They slip on assuming one vendor's model fits all, on waiting for health system build, and on treating availability as stable data. The common failure modes, and what each costs:

  • Building against the R4 spec, then discovering the vendor does not implement it. A team writes a clean Slot search and Appointment create, which works on Oracle Health, then learns Epic books through STU3 $find and $book and athenahealth through a proprietary API. The adapter gets rewritten twice. Read the vendor docs before the data model.
  • Waiting on Cadence build. Epic's $find returns nothing useful until the organization has built its rules. That work sits in the health system's queue, not yours, and it is often the longest item on the plan.
  • Hardcoding visit type codes. Codes that work at one site mean nothing at the next. Teams that shipped constants spend the second go-live refactoring into per-tenant configuration.
  • Assuming patient-facing access. Neither Epic's booking operations nor Oracle's create list patient authorization. An app designed around patient scopes needs a new authorization design.
  • No sync plan. Without SIU or polling, your product shows appointments staff already cancelled, and reminders go out for visits that no longer exist.
  • Unbounded searches. Wide date windows and per-patient polling burn rate limits. Our guide to FHIR search query patterns developers get wrong covers the fixes.

Production checklist for a FHIR scheduling API integration

Run this checklist before go-live at each health system. Each item maps to a failure mode described above, and each is cheaper to fix before live traffic than after.

  1. Confirm the booking path per EHR (R4 create, STU3 $find and $book, or proprietary API) and that your authorization context is permitted for each call.
  2. Store visit type codes, departments and self-scheduling rules per site as tenant configuration.
  3. Block booking on zero or multiple patient matches.
  4. Bound every availability search and follow next links instead of building page URLs.
  5. Use a client idempotency key per booking attempt and check for an existing appointment before any retry.
  6. Read the created Appointment back before confirming, and store its ID and ETag for If-Match updates.
  7. Handle hold expiry and slot conflicts by re-searching.
  8. Stand up a sync channel (SIU feed or scoped polling) for staff-made changes.
  9. For voice or AI agents, enforce verification, read-back and handoff in code, not only in the prompt.
  10. Audit-log requests, responses and confirmations, and alert on booking failure rates per site.

Scheduling sits at the point where EHR integration meets patient experience, and it is where multi-EHR products most often need a normalization layer; our post on a single API across Epic, Oracle Health and athenahealth shows how that layer is structured.

Building a scheduling product or voice agent that has to book into real EHRs? Our Healthcare Interoperability Solutions team builds the Epic, Oracle Health and athenahealth integrations, and our AI Agents for Healthcare practice designs the verification and handoff layer around them. Talk to our team to scope your first site.

Ready to scale?

Talk to our healthcare engineering team about building, integrating, and shipping faster.

Frequently Asked Questions

What is the difference between a FHIR Slot and a FHIR Appointment?

A Slot is a bookable interval of time on a Schedule, with a status such as free or busy and a required start and end. An Appointment is the booking itself: it references the Slot or Slots it fills, lists participants such as the patient, practitioner and location, and carries a status such as proposed, booked or cancelled. You search Slots to find time and create an Appointment to take it.

Does Epic support FHIR appointment booking?

Yes, through the STU3 Appointment $find and $book operations listed on fhir.epic.com. Both require pre-coordination: the health system must build rules in its Cadence scheduling system that decide which providers and slots are returned. Epic documents these operations for backend systems and clinician or administrative users. Epic's R4 Appointment search reads existing appointments for a known patient and does not find open time.

Can you book appointments with the athenahealth FHIR API?

Not through FHIR today. athenahealth's public FHIR R4 CapabilityStatement lists the Appointment resource with read and search interactions only, and it does not list Slot or Schedule. Open slots come from the proprietary GET /appointments/open endpoint, and booking is a PUT on the chosen slot's appointmentid with a patientid and a reasonid from the practice's configuration. Most products read appointments through FHIR and book through these athenaOne endpoints.

What is the Argonaut Scheduling Implementation Guide?

It is a US scheduling specification from the Argonaut Project that defines four operations: Slot $prefetch, Appointment $find, Appointment $hold and Appointment $book. Its only formal release is version 1.0.0, published in April 2018 on FHIR STU3. It remains useful as a design reference, and Epic's $find and $book operations follow the same pattern on STU3 endpoints.

How do you prevent double-booking with a FHIR scheduling API?

Treat every availability result as stale, and let the EHR decide who wins. Generate an idempotency key for each booking attempt, check whether an appointment already exists before retrying after a timeout, and re-search availability when a create fails. Store the ETag from each create and send If-Match on updates where the vendor requires it, as Oracle Health does for Appointment patch.

Can an AI voice agent book appointments directly into the EHR?

It can, using the same booking calls as any scheduling app, but the EHR cannot verify who is on the phone. The agent must verify identity before looking up the patient, limit requests to visit types the site allows for self-scheduling, read back the time and location for explicit confirmation, confirm the result from the EHR, and hand off to staff whenever any step fails.

How do you get notified when an appointment changes in the EHR?

The most widely available option is an HL7 v2 SIU feed, which carries events such as S12 new booking, S13 rescheduling, S15 cancellation and S26 no-show. Epic documents an Outgoing Appointment Scheduling interface for this. FHIR Subscription is not listed in the public R4 CapabilityStatements of Epic, Oracle Health or athenahealth, though athenahealth documents separate Event Notifications webhooks with Appointment topics. Elsewhere, FHIR-only sync usually means polling appointment searches over date windows.
Share