On this page

## Beta

The `$hold` operation is currently in [beta](/content/docs/compliance/alpha-beta/index.html).

The `$hold` operation places a hold on one or more schedules by atomically creating an [`Appointment`](/content/docs/api/fhir/resources/appointment/index.html) and [`Slot`](/content/docs/api/fhir/resources/slot/index.html) 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`](/content/docs/scheduling/appointment-find/index.html)** — 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`](/content/docs/scheduling/appointment-confirm/index.html)** or **[`$cancel`](/content/docs/scheduling/appointment-cancel/index.html)** — 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

```text
[base]/R4/Appointment/$hold
```

- TypeScript
- cURL

```ts
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'));  
```

```bash
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

- 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](/content/docs/scheduling/appointment-find/index.html).

### 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](/content/docs/scheduling/timezones/index.html).

```json
{
  "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`](/content/docs/api/fhir/resources/healthcareservice/index.html) 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`](/content/docs/scheduling/appointment-find/index.html) 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:

- One [`Appointment`](/content/docs/api/fhir/resources/appointment/index.html) with `status: "pending"`
- One `Slot` per contained slot parameter. Slots with status `"busy"` will be saved with status `"busy-tentative"`.

### Example Response

```json
{
  "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.

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

### Mismatched Slot Times

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

### No Scheduling Parameters Found

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

### Actor Missing Timezone

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