Appointment $hold | Medplum

On this page

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.

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

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

Name Type Description Required
appointment Appointment An Appointment resource describing the desired appointment time. Must include start, end, and serviceType. Must have Slot resources in contained. Yes

Constraints

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.

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.

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.

Output

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

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

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

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