Skip to main content

Appointment $book

Beta

The $book operation is currently in beta.

The $book operation books an Appointment by atomically creating the Appointment, one or more busy Slot resources, and any required buffer Slots in a single FHIR transaction. The operation validates that the requested time is genuinely available before committing.

Use Cases

  • Direct booking: Book an appointment directly from a $find result, without a prior hold
  • Multi-resource booking: Simultaneously book multiple Schedules (e.g., surgeon + OR room + anesthesiologist) for the same appointment time
  • Programmatic scheduling: Automate appointment creation from external systems while respecting provider availability rules

Invoke the $book operation

[base]/R4/Appointment/$book
import { isResource, MedplumClient } from '@medplum/core';
import type { Appointment, Bundle, Slot } from '@medplum/fhirtypes';

const medplum = new MedplumClient();

// 1. Find available appointments
const findUrl = medplum.fhirUrl('Appointment', '$find');
findUrl.searchParams.append('start', '2026-03-10T00:00:00Z');
findUrl.searchParams.append('end', '2026-03-10T23:59:59Z');
findUrl.searchParams.append('service-type-reference', 'HealthcareService/my-healthcare-service-id');
findUrl.searchParams.append('schedule', 'Schedule/my-schedule-id');
const findBundle = (await medplum.get<Bundle<Appointment>>(findUrl)) as Bundle;

// 2. Pick a proposed appointment from the results
const proposedAppointment = findBundle.entry?.[0]?.resource as Appointment;

// 3. Book it
const bundle = await medplum.post<Bundle<Appointment | Slot>>(medplum.fhirUrl('Appointment', '$book'), {
resourceType: 'Parameters',
parameter: [
{
name: 'appointment',
resource: proposedAppointment,
},
],
});

// Use the newly created Appointment resource
const appointment = bundle.entry?.map((e) => e.resource)?.find((e) => isResource<Appointment>(e, 'Appointment'));

Parameters

NameTypeDescriptionRequired
appointmentAppointmentA proposed Appointment resource (e.g. from $find). Must include start, end, and serviceType. Must have Slot resources in contained.Yes

Appointment Input

The appointment parameter accepts a proposed Appointment resource, exactly as returned by $find. The Appointment must include contained Slot resources that describe when to book each Schedule.

Every contained Slot must have status: "busy" (the booking itself) or status: "busy-unavailable" (a buffer), and must cover a positive duration. Any other status — or a Slot whose end is not after its start — is rejected, since $book has no defined capacity semantics for it.

{
"resourceType": "Parameters",
"parameter": [
{
"name": "appointment",
"resource": {
"resourceType": "Appointment",
"status": "proposed",
"start": "2026-03-10T09:00:00.000Z",
"end": "2026-03-10T10:00:00.000Z",
"serviceType": [
{
"coding": [{ "code": "initial-visit" }],
"extension": [
{
"url": "https://medplum.com/fhir/service-type-reference",
"valueReference": { "reference": "HealthcareService/my-healthcareservice-id" }
}
]
}
],
"participant": [
{ "actor": { "reference": "Practitioner/dr-smith" }, "required": "required", "status": "needs-action" }
],
"contained": [
{
"resourceType": "Slot",
"status": "busy",
"schedule": { "reference": "Schedule/dr-smith-schedule" },
"start": "2026-03-10T09:00:00.000Z",
"end": "2026-03-10T10:00:00.000Z"
}
]
}
}
]
}

For multi-resource bookings, include multiple Slot resources in Appointment.contained:

{
"resourceType": "Parameters",
"parameter": [
{
"name": "appointment",
"resource": {
"resourceType": "Appointment",
"status": "proposed",
"start": "2026-03-11T08:00:00.000Z",
"end": "2026-03-11T10:00:00.000Z",
"serviceType": [
{
"coding": [{ "code": "bariatric-surgery" }],
"extension": [
{
"url": "https://medplum.com/fhir/service-type-reference",
"valueReference": { "reference": "HealthcareService/my-healthcareservice-id" }
}
]
}
],
"participant": [
{ "actor": { "reference": "Practitioner/dr-smith" }, "required": "required", "status": "needs-action" },
{ "actor": { "reference": "Location/or-room-1" }, "required": "required", "status": "needs-action" }
],
"contained": [
{
"resourceType": "Slot",
"status": "busy",
"schedule": { "reference": "Schedule/surgeon-schedule-id" },
"start": "2026-03-11T08:00:00.000Z",
"end": "2026-03-11T10:00:00.000Z"
},
{
"resourceType": "Slot",
"status": "busy",
"schedule": { "reference": "Schedule/or-room-schedule-id" },
"start": "2026-03-11T08:00:00.000Z",
"end": "2026-03-11T10:00:00.000Z"
}
]
}
}
]
}

Constraints

  • Each referenced Schedule must have exactly one actor
  • Each actor must have a timezone defined via the http://hl7.org/fhir/StructureDefinition/timezone extension
  • The requested time must match a valid slot duration from the Schedule's SchedulingParameters
  • The requested time must have fewer slots than the requested slotCapacity. All existing slots overlapping the requested time must be below their maximum slotCapacity. See Overbooking
  • The serviceType attribute must reference the HealthcareService you are trying to schedule via the https://medplum.com/fhir/service-type-reference extension
  • The input Appointment must not already contain slot references (these are set by $book)

The easiest way to meet these requirements is to use a result from a $find operation.

Output

Returns 201 Created with a Bundle wrapping all persisted resources:

  • One Appointment with status: "booked"
  • One Slot per contained Slot with status: "busy"
  • Zero or more buffer Slot resources with status: "busy-unavailable" (when bufferBefore or bufferAfter scheduling parameters are set)

Example Response

{
"resourceType": "Bundle",
"type": "transaction-response",
"entry": [
{
"resource": {
"resourceType": "Appointment",
"id": "new-appointment-id",
"status": "booked",
"start": "2026-03-10T09:00:00.000Z",
"end": "2026-03-10T10:00:00.000Z",
"participant": [
{ "actor": { "reference": "Practitioner/dr-smith" }, "status": "tentative" }
],
"slot": [{ "reference": "Slot/booked-slot-id" }]
}
},
{
"resource": {
"resourceType": "Slot",
"id": "booked-slot-id",
"status": "busy",
"start": "2026-03-10T09:00:00.000Z",
"end": "2026-03-10T10:00:00.000Z",
"schedule": { "reference": "Schedule/dr-smith-schedule" }
}
}
]
}

Booking Logic

$book performs the following steps atomically inside a database transaction, ensuring safety when concurrent booking requests are received.

  1. Validates that each proposed Slot's start/end matches a valid slot duration defined in the Schedule's SchedulingParameters
  2. Loads existing Slots in the time window (including buffer margins) for each Schedule
  3. Checks that the requested time has spare capacity under the strictest applicable limit — the requested slotCapacity and the tolerance of every busy/busy-tentative booking already overlapping it (at the default capacity of 1, any overlapping busy Slot blocks it), and no busy-unavailable block or buffer conflicts
  4. Verifies the requested time falls within the Schedule's defined availability windows or existing slots with status free
  5. Creates the Appointment, busy Slot(s), and any buffer Slot(s)
  6. Returns all created resources in the response Bundle

Because these steps run inside a SERIALIZABLE transaction, two requests racing for the last unit of capacity cannot both succeed — one commits and the other is rejected. An outstanding $hold also consumes a unit of slotCapacity (via its busy-tentative Slot) until it is confirmed, booked, or released.

Error Responses

Time Not Available

{
"resourceType": "OperationOutcome",
"issue": [{ "severity": "error", "code": "invalid", "details": { "text": "Requested time slot is not available" } }]
}

Mismatched Slot Times

{
"resourceType": "OperationOutcome",
"issue": [{ "severity": "error", "code": "invalid", "details": { "text": "Mismatched slot start times" } }]
}

Actor Missing Timezone

{
"resourceType": "OperationOutcome",
"issue": [{ "severity": "error", "code": "invalid", "details": { "text": "No timezone specified" } }]
}

Beta Status

The Scheduling API is under active development. This beta release of the scheduling API is expected to gain additional capabilities.

  • bookingLimit - An upcoming scheduling parameter that will allow you to express how often a given service type may be added to a schedule. This is not yet enforced in $book.