Resources
Cerner Scheduling API: Booking Against a Slot
By Federico Ruiz Cassarino, CEO and founder, Puppeteer AI · · 6 min read
Cerner, now Oracle Health, speaks FHIR, which makes it look familiar to anyone who has integrated with Canvas or Medplum. The resources have the same names. The parameters don't behave the same way, and a slot search that works verbatim against another FHIR server will return nothing here.
This is a walkthrough of the differences that actually cost time.
The slot search wants more than you'd expect
Finding open time is a GET /Slot search, and it takes four things, three of which are unusual.
GET /Slot?-location=Location/{id}&schedule.actor=Practitioner/{id}&service-type={url}&slot-type={url}&start=ge{date}T00:00:00.000Z&start=lt{date}T23:59:59.000Z
- The location filter carries a leading hyphen.
-location, notlocation. It's a valid FHIR search modifier, not a typo, and it's easy to normalize away in a client library that tidies parameter names. - Two type filters, set to the same value.
service-typeandslot-typeboth have to be set. Setting only one is the natural guess, and it doesn't work. - The type value is a fully qualified URL, not a bare code. It's built from your tenant's FHIR base URL, your tenant id, the literal path segment
codeSet, and a code set number and an appointment type code joined by a pipe:
{fhir_url}{tenant_id}/codeSet/14249|24477854
14249 is the scheduling appointment code set. 24477854 is an appointment type within it, Established Patient. Both are values you look up for your tenant rather than constants you can hard-code across deployments.
Timestamps are explicit and zoned too. The range uses ge with a full T00:00:00.000Z and lt with T23:59:59.000Z, repeated on the same start key. Bare dates don't behave the way you want.
Booking binds to the slot you found
Once you have a slot, creating the appointment is a POST /Appointment, but the body has two properties that differ from the FHIR you may be used to.
- Times go in requestedPeriod, not start and end. The body carries
requestedPeriodas an array of periods, rather than top-levelstartandendfields. Sending those top-level is the single most common cause of a booking that's rejected for reasons that aren't obvious from the error. - The slot is referenced explicitly. The appointment carries
slot: [{ reference }], pointing at the slot resource you retrieved. This is what ties the booking to real availability rather than asserting a time and hoping the schedule agrees.
The rest is conventional: status: "booked", a participant array with the patient as an actor reference at status: "accepted", and a serviceType carrying the full coding, a code, a display, and the system URL pointing back at the tenant's code set.
That slot binding is the meaningful design difference. On Healthie you assemble an appointment from a date and a set of ids. Here you must first obtain a slot object, then book against it. It's more round trips, and considerably fewer ways to create an appointment that doesn't correspond to anything on the calendar.
What a cancellation workflow looks like here
The slot binding shapes the whole flow, and mostly in your favor.
When an appointment is cancelled, the underlying slot becomes available again. Your workflow re-runs the slot search for that practitioner, location, and service type, and the freed time reappears as a real slot object with its own reference. You then book a waiting patient against that reference.
Compare that to an EHR where you assert a time and hope. Here, if the slot search doesn't return the freed time, it genuinely isn't bookable, whether because of a provider block, an overlapping appointment, or a schedule that was never configured to offer that service type at that hour. You find out before you contact a patient rather than after they accept.
The cost is round trips. Every offer needs a fresh slot search, because a slot reference retrieved five minutes ago may already be taken. Booking against a stale reference fails, which is correct behavior and still something your code has to handle gracefully rather than surfacing as an error to whoever is on the phone.
Build the flow as search-then-book, close together, and treat a failed booking as search again rather than as a fault.
Reading a patient's appointments
Straightforward, with the same timestamp discipline:
GET /Appointment?patient={id}&date=ge{start}T00:00:00.000Z&date=lt{end}T23:59:59.000Z&_count={n}
_count controls page size. Set it deliberately, because the default won't be what you assume, and a patient with a long history will page.
Note that patient here takes a bare id rather than a Patient/{id} reference, the opposite of how -location and schedule.actor want their values. That inconsistency is worth a comment in your own code, because you will get it backwards at least once.
Everything is tenant-scoped
The tenant id isn't a header or a token claim. It's baked into the FHIR base URL, and it appears again inside the code set values you send as search parameters.
Two consequences follow. Configuration is per tenant, including code set values that look like constants but aren't. And anything you build for one health system needs its identifiers re-derived, rather than copied, when it moves to the next.
Treat the code set URL as configuration, assembled at runtime from the tenant's base URL and id. Hard-coding the assembled string is the shortcut that works in one environment and fails silently in the next.
What you need on your side
- Tenant credentials and the tenant's FHIR base URL. Both feed the code set values you'll need for every slot search.
- Code set values for the appointment types you schedule. Look them up per tenant. Don't assume the defaults transfer.
- Location and practitioner ids. Required for slot searches.
- An HTTP client that leaves parameter names alone. One that also supports repeating a key with different prefixes. Both requirements are unusual enough to break naive clients built against other FHIR servers.
What access actually costs
The technical work above is the cheap part, and it's worth knowing that before you scope a Cerner project.
Oracle publishes the number, which not every large vendor does. Registering an application and taking it to production carries a one-time US$10,000 charge, covering one production environment and one test environment. Additional test environments run US$5,000 each. After that there's no recurring charge for usage of the certified APIs. The cost is a gate, not a meter.
A gate is a different planning problem from a meter. It means a Cerner integration doesn't get cheaper by starting small, so the sensible move is to have more than one health system lined up before paying it, rather than treating it as the cost of an experiment. It also means the number is knowable in advance, which puts Oracle ahead of several vendors in this series who won't name a price until you're deep in a sales conversation.
The other cost isn't billed to you at all: the health system's own approval process to let a new vendor into their environment. That's measured in months, it varies by organization, and no amount of budget shortens it.
Honest limits
- Slot searches are narrow by construction. Location, practitioner, and service type are all required in practice, so a practice-wide availability sweep is many queries, not one.
- Nothing here is a waitlist. Cerner's scheduling surface exposes availability and bookings; the list of patients wanting an earlier slot lives in your own system.
- Availability reflects schedule configuration. Time that isn't modeled as a bookable slot for that service type won't appear, however empty the calendar looks.
- Access is the long pole. Getting production API access to a health system's Cerner instance is an organizational process with a five-figure entry fee, not a signup. Plan the integration timeline around that, not around the code.
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.