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: bookedand exactly one slot reference. - Epic books through STU3
Appointment/$findand$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/opento find slots andPUT /appointments/{appointmentid}to book one. - None of the three public R4 CapabilityStatements lists
_lastUpdatedfor 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.
| Resource | What it represents | Key elements (R4 cardinality) | Search parameters defined in R4 |
|---|---|---|---|
| Schedule | The calendar container for one actor: a Practitioner, Location, HealthcareService or Device | actor 1..*, planningHorizon 0..1, active 0..1, serviceType 0..* | active, actor, date, identifier, service-category, service-type, specialty |
| Slot | One bookable interval on a Schedule | schedule 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..1 | appointment-type, identifier, schedule, service-category, service-type, specialty, start, status |
| Appointment | The booking itself | status 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 |
| AppointmentResponse | One participant's reply to a proposed appointment | appointment 1..1, participantStatus 1..1 (accepted, declined, tentative, needs-action), actor 0..1 | actor, 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.
- 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.
- 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. - Hold the time if supported. The Argonaut IG defines
$hold, and Epic holds$findresults temporarily. Oracle Health documents no hold operation. Its Slot patch can set status tobusy-tentativeor back tofreeunder Provider or System authorization, but Oracle does not describe that as a hold, so treat the slot as yours only once the create succeeds. - Book. POST an Appointment that references the Slot with
status: booked, or call$bookwith the proposed appointment ID. - Confirm. Read the created Appointment back from the
Locationheader or the operation response. Check status, start, end, practitioner and location against what the patient agreed to. - 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
Expiresheader. Build a countdown, not an assumption. - Rescheduling is cancel plus rebook.
$booktakes an optionalcancelled-appt-id, so a reschedule is one call that books the new time and releases the old one. - The availability answer is a proposal.
$findreturns proposed Appointments, and a successful$holdmoves one topending(orcancelledif 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.
| Capability | Epic | Oracle Health (Millennium R4) | athenahealth |
|---|---|---|---|
| Availability search | POST 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 only | No Slot or Schedule in FHIR R4. Proprietary GET /v1/{practiceid}/appointments/open with departmentid and a reasonid or appointmenttypeid |
| Booking method | POST Appointment/$book (STU3) with patient ID, appointment ID from $find and an optional note | POST Appointment with status: booked, one slot, patient participant accepted. Or status: proposed with a requestedPeriod | No FHIR create. Proprietary PUT /v1/{practiceid}/appointments/{appointmentid} on an open slot, with patientid and reasonid |
| Cancel and reschedule | No 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 appointments | JSON Patch: replace /status with cancelled plus /cancelationReason. Reschedule by replacing /slot with a reschedule-reason extension. If-Match required | Proprietary PUT .../appointments/{appointmentid}/cancel and PUT .../appointments/{appointmentid}/reschedule with newappointmentid |
| Patient required on Appointment search | Yes. R4 Appointment.Search marks patient as required | One of patient, practitioner or location (never combined), or _id; with date or -date-or-req-period | CapabilityStatement 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 patients | Create and patch support Provider and System authorization, not Patient | Apps approved for the athenaOne appointment APIs, which need a Platform Services contract and Solution Validation for production |
| Notes | Requires pre-coordination: the organization must build rules in Cadence. $find pages 10 results by default and does not return existing appointments | Patient-authorized Appointment search does not return sensitive appointments | FHIR 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
$findaccepts 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.
| Approach | How it works | What limits it in practice | Use it when |
|---|---|---|---|
| Polling FHIR Appointment search | Repeat searches by patient, practitioner or location over a date window and diff the results | R4 defines _lastUpdated, but none of the three CapabilityStatements lists it for Appointment. Epic requires patient, so polling scales per patient. Rate limits apply | Low volume, patient-centric apps, or confirming a single booking |
| HL7 v2 SIU feed | The EHR sends SIU messages for scheduling events to your interface engine | Needs an interface build with each health system and an engine to receive, parse and map messages | Provider or department-wide calendars, reminders, waitlists and no-show workflows |
| FHIR Subscription | The server notifies you when matching resources change | Not 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 servers | Where 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-Matchwith 412 and a stale version with 409. Store the ETag from every create and read. - Expired hold. An Epic
$findresult is held only temporarily, and an Argonaut hold ends when the server'sExpirestime 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
$findaccepts 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
$findand$bookand athenahealth through a proprietary API. The adapter gets rewritten twice. Read the vendor docs before the data model. - Waiting on Cadence build. Epic's
$findreturns 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.
- Confirm the booking path per EHR (R4 create, STU3
$findand$book, or proprietary API) and that your authorization context is permitted for each call. - Store visit type codes, departments and self-scheduling rules per site as tenant configuration.
- Block booking on zero or multiple patient matches.
- Bound every availability search and follow
nextlinks instead of building page URLs. - Use a client idempotency key per booking attempt and check for an existing appointment before any retry.
- Read the created Appointment back before confirming, and store its ID and ETag for
If-Matchupdates. - Handle hold expiry and slot conflicts by re-searching.
- Stand up a sync channel (SIU feed or scoped polling) for staff-made changes.
- For voice or AI agents, enforce verification, read-back and handoff in code, not only in the prompt.
- 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.



