Skip to main content

Appointment $book

Alpha

The $book operation is currently in alpha.

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 which Schedules to book.

{
"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
  • No existing busy Slots may overlap the requested time window (including buffer windows)
  • 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:

  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 no existing busy Slot overlaps the requested time
  4. Verifies the requested time falls within the Schedule's defined availability windows
  5. Creates the Appointment, busy Slot(s), and any buffer Slot(s) atomically
  6. Returns all created resources in the response Bundle

The transaction uses serializable isolation to prevent double-booking under concurrent requests.

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" } }]
}