On this page

Appointment $hold

Beta

The $hold operation is currently in beta.

The $hold operation places a hold on one or more schedules by atomically creating an Appointment and Slot resources. The operation validates that the requested time is genuinely available before committing. It can also hold every occurrence of a weekly recurring series in the same transaction.

Booking Lifecycle​

$hold is the second step in a three-step booking flow:

  1. $find — Query available time slots. Returns virtual Appointment resources with contained Slot resources.
  2. $hold — Submit one of those virtual Appointments to reserve the time. Creates a real Appointment (status: pending) and Slot (status: busy-tentative).
  3. $confirm or $cancel — Confirm the hold and transition the Appointment to booked, or cancel the hold and transition the Appointment to cancelled.

Use Cases​

  • Patient self-serve booking: Reserve a slot from $find results while the patient confirms their details, before committing to a booked appointment.
  • Multi-provider holds: Atomically hold time across multiple schedules (e.g., surgeon + OR room) before requiring patient confirmation.
  • Unconfirmed booking flows: Create a pending appointment that staff can review and confirm via $book.

Invoke the $hold operation​

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

const medplum = new MedplumClient();

const result = await medplum.post<Bundle<Appointment | Slot>>(medplum.fhirUrl('Appointment', '$hold'), {
  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',
          } satisfies Slot,
        ],
      } satisfies Appointment,
    },
  ],
});

const appointment = result.entry?.map((e) => e.resource).find((r) => isResource<Appointment>(r, 'Appointment'));
curl -X POST 'https://api.medplum.com/fhir/R4/Appointment/$hold' \
  -H "Content-Type: application/fhir+json" \
  -H "Authorization: Bearer MY_ACCESS_TOKEN" \
  -d '{
    "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"
            }
          ]
        }
      }
    ]
  }'

Parameters​

NameTypeDescriptionRequired
appointmentAppointmentAn Appointment resource describing the desired appointment time. Must include start, end, and serviceType. Must have Slot resources in contained. May carry a recurrenceTemplate to hold a weekly series; see Holding a recurring series.Yes

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 $hold)

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

Schedule.Actor must have a TimeZone​

Scheduling checks availability for each Actor with respect to their local time zone. For more details, see Time Zones.

{
  "resourceType": "Practitioner",
  "extension": [
    {
      "url": "http://hl7.org/fhir/StructureDefinition/timezone",
      "valueCode": "America/New_York"
    }
  ]
}

Appointment.serviceType must reference a HealthcareService​

Medplum Scheduling starts from a HealthcareService as a representation of a schedulable appointment type. Medplum Scheduling requires embedding a reference to the specific HealthcareService in an extension.

Why this extension?

While codes are a very flexible way to build connections to services, they also create a lot of opportunity for ambiguity. Medplum Scheduling has decided to use explicit references to specific HealthcareServices to resolve that tension.

In FHIR R5+ the type of these serviceType fields is CodeableReference(HealthcareService), meaning that it will eventually be valid to store a Reference<HealthcareService> in this attribute directly. In the meantime, in our FHIR R4 implementation we mimic this feature by using a custom extension.

{
  "resourceType": "Appointment",
  "serviceType": [
    {
      "coding": [{ "code": "bariatric-surgery" }],
      "extension": [
        {
          "url": "https://medplum.com/fhir/service-type-reference",
          "valueReference": { "reference": "HealthcareService/my-healthcareservice-id" }
        }
      ]
    }
  ]
}

Appointment.contained holds virtual Slot resources​

The $hold endpoint expects that its input describe exactly what slots should be created, including references to specific Schedule resources. The easiest way to construct a valid input is to pass an Appointment returned by $find directly — $find already populates contained with the correct virtual Slot resources.

These slots will be removed from the Appointment.contained array when they are persisted to real Slot resources during $hold.

Multi-Schedule Holds​

Pass multiple virtual Slot resources in Appointment.contained to hold time across multiple Schedules atomically. All slots must share the same start and end time.

{
  "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/dr-smith-schedule" },
            "start": "2026-03-11T08:00:00.000Z",
            "end": "2026-03-11T10:00:00.000Z"
          },
          {
            "resourceType": "Slot",
            "status": "busy",
            "schedule": { "reference": "Schedule/or-room-schedule" },
            "start": "2026-03-11T08:00:00.000Z",
            "end": "2026-03-11T10:00:00.000Z"
          }
        ]
      }
    }
  ]
}

Output​

Returns 201 Created with a response body containing a Bundle of all persisted resources:

  • One Appointment with status: "pending", or one per occurrence for a recurring series
  • One Slot per contained slot parameter. Slots with status "busy" will be saved with status "busy-tentative".

When holding a recurring series, the Bundle holds one pending Appointment per occurrence, in order, each followed by its own Slots.

Example Response​

{
  "resourceType": "Bundle",
  "type": "transaction-response",
  "entry": [
    {
      "resource": {
        "resourceType": "Appointment",
        "status": "pending",
        "start": "2026-03-14T16:00:00.000Z",
        "end": "2026-03-14T16:45:00.000Z",
        "participant": [
          {
            "actor": {
              "reference": "Practitioner/348708c1-6a8e-4405-b36b-93f590ed7948"
            },
            "status": "needs-action"
          }
        ],
        "serviceType": [
          {
            "extension": [
              {
                "url": "https://medplum.com/fhir/service-type-reference",
                "valueReference": {
                  "reference": "HealthcareService/23c3f1cc-4f55-4990-9775-511b02487e7e"
                }
              }
            ]
          }
        ],
        "slot": [
          {
            "reference": "Slot/52fbef8f-e9e6-4810-be53-e481649a72d8"
          }
        ],
        "id": "bdd87182-a05d-4ccc-ae7b-2ecd3f6049df",
        "meta": {
          "versionId": "0cb595fc-35d2-46be-9104-35332d6177e6",
          "lastUpdated": "2026-05-05T23:50:57.302Z"
        }
      }
    },
    {
      "resource": {
        "resourceType": "Slot",
        "start": "2026-03-14T16:00:00.000Z",
        "end": "2026-03-14T16:45:00.000Z",
        "schedule": {
          "reference": "Schedule/cc26e4b9-a92d-420c-873d-82e7d20152e7"
        },
        "status": "busy-tentative",
        "id": "52fbef8f-e9e6-4810-be53-e481649a72d8",
        "meta": {
          "versionId": "46689bb4-bc6d-4ac6-a917-cfe739a1ecd6",
          "lastUpdated": "2026-05-05T23:50:57.294Z"
        }
      }
    }
  ]
}

Holding a recurring series​

Passing a recurring series entry from $find holds every occurrence of the series, all or none. Pass the entry unchanged: its recurrenceTemplate extension tells $hold how the series recurs.

$hold expands the series exactly as $book does: it reads the template under the same template requirements, and tags every occurrence with the same series identifier and recurrenceId. Each occurrence is created pending, with its own busy-tentative Slot.

Hold Logic​

$hold performs the following steps atomically inside a database transaction, ensuring safety when concurrent scheduling 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 no existing busy Slot overlaps the requested time
  4. Verifies the requested time falls within the Schedule's defined availability windows or existing slots with status free
  5. Creates the Appointment, busy-tentative Slot(s), and any buffer Slot(s)
  6. Returns all created resources in the response Bundle

When holding a recurring series, steps 1–5 run for each occurrence in turn, in the same transaction, so each occurrence is checked against the ones held before it. If any occurrence is unavailable, the whole transaction rolls back and nothing is held.

Error Responses​

All error responses return HTTP 400 with an OperationOutcome body.

Time Not Available​

Returned when the requested time overlaps an existing busy Slot or falls outside the Schedule's defined availability windows.

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

No Scheduling Parameters Found​

{
  "resourceType": "OperationOutcome",
  "issue": [{ "severity": "error", "code": "invalid", "details": { "text": "No SchedulingParameters found on Schedule or HealthcareService" } }]
}

Actor Missing Timezone​

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

HealthcareService is inactive​

{
  "resourceType": "OperationOutcome",
  "issue": [{ "severity": "error", "code": "invalid", "details": { "text": "HealthcareService is inactive" } }]
}

Unsupported Recurrence Template​

Returned when a recurrenceTemplate doesn't meet the template requirements. See $book's error for its shape.