---
title: Booking an appointment end-to-end
description: Discover a bookable service, a practitioner and their room, a free slot, then POST a FHIR R4 Appointment — with the booking preconditions the discovery steps exist to satisfy.
nav: Recipes
order: 20
version: v1
source: handwritten
updated: 2026-06-26
---

# Booking an appointment end-to-end

Turn a "who is free, and for what?" question into a stored `Appointment`. You will discover a
bookable service, find a practitioner and the room they work in, locate a free slot, and
`POST` the booking — assembling exactly the four references the create call needs to pass its
preconditions. Two scopes carry the whole flow: <Scope>system/Appointment.cru</Scope> for the
write (and the discovery resources gated behind it) and <Scope>system/Practitioner.rs</Scope>
for the practitioner wiring.

A blind `POST /fhir/R4/Appointment` rarely succeeds on the first try: the server rejects a
booking whose `serviceType` is unknown, whose practitioner carries no location, or whose room
sits outside the practitioner's assigned rooms. The discovery steps below exist precisely to
hand you values that satisfy each of those checks, so the final write goes through.

## Audience

You build a patient-booking flow — a portal, a referral intake, or a front-desk tool — and
you have already run [your first authenticated search](/v1/recipes/getting-started-patient-search).
You read a `Bundle` without a viewer, you know what a FHIR reference is, and you want to take
a booking from discovery to a `201 Created`.

## You'll need

- A bearer token from HuliPractice (**Practice Settings → Integrations → API Keys**), or a
  SMART Backend Services access token. See [Bearer Tokens](/v1/auth/bearer) for provisioning
  and [`POST /auth/token`](/v1/auth) for the token exchange — this recipe assumes you already
  hold one.
- These two scopes on that token:
  - <Scope name="system/Appointment.cru" /> — create `Appointment` (`.cru` also grants read +
    search). The discovery resources `HealthcareService`, `Location`, `Schedule`, and `Slot`
    are gated behind the Appointment scope, so this one grant covers them.
  - <Scope name="system/Practitioner.rs" /> — read + search `Practitioner` and
    `PractitionerRole`.
- `curl`, or Node, Python, Java, or Go if you prefer a language client.

<Callout variant="info">
The discovery resources page on offset pagination (`_count` + `_offset`), not the keyset
`_cursor` that Patient and Appointment search use. `_count` defaults to 20 and caps at 100;
walk pages by adding `_offset` in multiples of `_count`. The `Slot` and `Schedule` searches
follow the same offset rule.
</Callout>

## End state

You hold a `201 Created` whose body is the stored `Appointment` — booked for a real
practitioner, in a room that practitioner actually works in, at a slot that was free, for a
service the organization offers. Along the way you have the four values the create call
consumed: the `org-service` serviceType coding, the `Practitioner` reference, the room
`Location` reference, and the slot's `start`/`end`.

## Steps

### 1. Export the token

```bash
export HULI_TOKEN="<paste your bearer token here>"
```

### 2. Discover a bookable service

<Endpoint method="GET" path="/fhir/R4/HealthcareService?_count=50" />

Each org service-catalog entry is one `HealthcareService`. The value you need is the entry's
`type.coding` — its `code` is the org-service UUID, published under the `org-service`
CodeSystem. **That coding is exactly what you put in `Appointment.serviceType`** when you
book; copy it verbatim, do not rebuild it from the name.

:::CodeGroup

```bash
curl "https://api.huli.ai/fhir/R4/HealthcareService?_count=50" \
  -H "Authorization: Bearer $HULI_TOKEN" \
  -H "Accept: application/fhir+json"
```

```typescript
const resp = await fetch('https://api.huli.ai/fhir/R4/HealthcareService?_count=50', {
  headers: {
    Authorization: `Bearer ${process.env.HULI_TOKEN}`,
    Accept: 'application/fhir+json',
  },
});

const bundle = await resp.json();
// The serviceType coding you book with is entry.resource.type[0].coding[0].
const service = bundle.entry?.[0]?.resource;
const serviceType = service?.type?.[0];
console.log(JSON.stringify(serviceType, null, 2));
```

```python
import os
import requests

resp = requests.get(
    "https://api.huli.ai/fhir/R4/HealthcareService",
    params={"_count": 50},
    headers={
        "Authorization": f"Bearer {os.environ['HULI_TOKEN']}",
        "Accept": "application/fhir+json",
    },
    timeout=30,
)
bundle = resp.json()
# The serviceType coding you book with is entry.resource.type[0].
service = bundle["entry"][0]["resource"]
service_type = service["type"][0]
print(service_type)
```

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class DiscoverService {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.huli.ai/fhir/R4/HealthcareService?_count=50"))
            .header("Authorization", "Bearer " + System.getenv("HULI_TOKEN"))
            .header("Accept", "application/fhir+json")
            .GET()
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        // entry[].resource.type[0].coding[0] carries the org-service code you
        // place in Appointment.serviceType. Parse with a JSON library in real code.
        System.out.println(response.body());
    }
}
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest(http.MethodGet,
		"https://api.huli.ai/fhir/R4/HealthcareService?_count=50", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("HULI_TOKEN"))
	req.Header.Set("Accept", "application/fhir+json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	// entry[].resource.type[0].coding[0] holds the org-service serviceType code.
	fmt.Printf("%s\n", body)
}
```

:::

<RunnableRequest method="GET" path="/fhir/R4/HealthcareService?_count=50" recipe="booking-an-appointment" captures='[{"name":"serviceTypeCode","path":"entry.0.resource.type.0.coding.0.code"},{"name":"serviceTypeDisplay","path":"entry.0.resource.type.0.coding.0.display"}]' />

A representative `HealthcareService` entry inside the `searchset`:

```json
{
  "resourceType": "HealthcareService",
  "id": "01965e2a-8c4d-7000-9010-0000000000f1",
  "active": true,
  "name": "Consulta general",
  "type": [
    {
      "coding": [
        {
          "system": "https://fhir.huli.ai/r4/CodeSystem/org-service",
          "code": "01965e2a-8c4d-7000-9010-0000000000f1",
          "display": "Consulta general"
        }
      ],
      "text": "Consulta general"
    }
  ],
  "specialty": [
    {
      "coding": [
        {
          "system": "https://fhir.huli.ai/r4/CodeSystem/specialty",
          "code": "01965e2a-8c4d-7000-9011-0000000000d1",
          "display": "Medicina general"
        },
        {
          "system": "http://snomed.info/sct",
          "code": "394814009",
          "display": "General practice"
        }
      ],
      "text": "Medicina general"
    },
    {
      "coding": [
        {
          "system": "https://fhir.huli.ai/r4/CodeSystem/specialty",
          "code": "01965e2a-8c4d-7000-9011-0000000000d2",
          "display": "Pediatría"
        },
        {
          "system": "http://snomed.info/sct",
          "code": "394537008",
          "display": "Pediatrics"
        }
      ],
      "text": "Pediatría"
    }
  ],
  "providedBy": {
    "reference": "Organization/01965e2a-8c4d-7000-9001-0000000000b0"
  },
  "location": [
    {
      "reference": "Location/01965e2a-8c4d-7000-9020-0000000000a1",
      "display": "Consultorio 1"
    }
  ]
}
```

Two fields beyond `type` matter for booking. `specialty[]` lists **every** specialty the
service is offered for — one `CodeableConcept` per specialty. Each one carries a **bookable**
coding under the `specialty` CodeSystem (`system` =
`https://fhir.huli.ai/r4/CodeSystem/specialty`, `code` = the specialty UUID) plus, when the
catalog has one, a human SNOMED coding. The example above offers two specialties (general
medicine and pediatrics), so it is a **multi-specialty service**: when you book it you MUST pick
one and send its `specialty` coding verbatim in `Appointment.specialty` (step 6) — omitting the
selection is rejected (see **What can go wrong**). A service with a single specialty, or an empty
`specialty[]` (offered for all specialties), derives the specialty server-side and needs no
selection. Separately, every practitioner you book must carry the chosen specialty in their
`PractitionerRole.specialty`. The `location[]` array lists the rooms the service is offered in —
useful coverage context, but the _authoritative_ room set for the booking is the practitioner's,
which you read next.

<Callout variant="info">
**Shortcut — let the service hand you the valid practitioner/room pairs.** Rather than guessing
which practitioner works in which room, search `Schedule` by the service you just discovered:
every schedule configured to deliver that service binds a practitioner (its `PractitionerRole`
actor) to the room they serve it in (its `Location` actor). Each returned schedule is therefore a
practitioner/room **combination the booking will accept** — pick one and you sidestep the
"practitioner has no location" and "room outside the practitioner's rooms" rejections.

<Endpoint method="GET" path="/fhir/R4/Schedule?service-type=01965e2a-8c4d-7000-9010-0000000000f1" />

```bash
curl "https://api.huli.ai/fhir/R4/Schedule?service-type=01965e2a-8c4d-7000-9010-0000000000f1" \
  -H "Authorization: Bearer $HULI_TOKEN" \
  -H "Accept: application/fhir+json"
```

The `service-type` value is the org-service `code` from `HealthcareService.type[0].coding[0]`
(step 2) — `service-type` is the FHIR-standard Schedule search parameter that targets
`Schedule.serviceType`. Each entry's `Schedule.actor` carries a `PractitionerRole/<id>` and a
`Location/<id>`; lift that pair and jump straight to step 5 to find a free `Slot` for the
schedule. A schedule with an empty service set serves every service and is returned for any
`service-type` query that names a live org service (an unknown service id returns an empty
bundle). This axis needs only `system/Appointment.rs`; steps 3–4 below are the longer
practitioner-first path (they also read `Practitioner`/`PractitionerRole`, so they additionally
need `system/Practitioner.rs`). Use whichever fits your flow.
</Callout>

### 3. Find a practitioner and their wiring

Search for the practitioner, then read their `PractitionerRole` — that role names the rooms
the practitioner works in and the specialties they carry.

<Endpoint method="GET" path="/fhir/R4/Practitioner?name=Fern%C3%A1ndez" />

```bash
curl "https://api.huli.ai/fhir/R4/Practitioner?name=Fern%C3%A1ndez" \
  -H "Authorization: Bearer $HULI_TOKEN" \
  -H "Accept: application/fhir+json"
```

<RunnableRequest method="GET" path="/fhir/R4/Practitioner?_count=1" recipe="booking-an-appointment" captures='[{"name":"practitionerUserId","path":"entry.0.resource.id"}]' />

<Callout variant="note">
The Run button above searches without a name filter (`_count=1`) so it resolves to *some*
practitioner in your sandbox rather than requiring one literally named "Fernández" — swap in
`?name=…` once you know who you're booking.
</Callout>

With the practitioner's id in hand, read their role wiring:

<Endpoint method="GET" path="/fhir/R4/PractitionerRole?practitioner=01965e2a-8c4d-7000-9001-0000000000c1" />

```bash
curl "https://api.huli.ai/fhir/R4/PractitionerRole?practitioner=01965e2a-8c4d-7000-9001-0000000000c1" \
  -H "Authorization: Bearer $HULI_TOKEN" \
  -H "Accept: application/fhir+json"
```

<RunnableRequest method="GET" path="/fhir/R4/PractitionerRole?practitioner={{practitionerUserId}}" recipe="booking-an-appointment" captures='[{"name":"practitionerRoleId","path":"entry.0.resource.id"},{"name":"roomLocationRef","path":"entry.0.resource.location.0.reference"}]' />

A `PractitionerRole` resource:

```json
{
  "resourceType": "PractitionerRole",
  "id": "01965e2a-8c4d-7000-9030-0000000000b1",
  "active": true,
  "practitioner": {
    "reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1"
  },
  "organization": {
    "reference": "Organization/01965e2a-8c4d-7000-9001-0000000000b0"
  },
  "location": [
    {
      "reference": "Location/01965e2a-8c4d-7000-9020-0000000000a1",
      "display": "Consultorio 1"
    }
  ],
  "specialty": [
    {
      "coding": [
        {
          "system": "http://snomed.info/sct",
          "code": "394814009",
          "display": "General practice"
        }
      ],
      "text": "Medicina general"
    }
  ]
}
```

Three things to lift from the role: the `location[]` (the rooms this practitioner works in —
pick one in step 4), the `specialty[]` (it must include the service's specialty if step 2
carried one), and the `PractitionerRole.id` — that id is the schedulable resource you query
slots against in step 5.

<Callout variant="warning">
**Book against the `PractitionerRole.id`, not the `Practitioner` it points at.** When you POST the
Appointment (step 6), `participant[].actor.reference` carries the **`PractitionerRole.id`** you just
discovered, under a `Practitioner/` reference. The practitioner-user id that
`PractitionerRole.practitioner` references is **not** a schedulable resource; booking against it
returns `404`. The reference *type* is `Practitioner` (FHIR conformance), but the *id-space* is the
practitioner-role / schedulable resource — the same id `Schedule` and `Slot` reference.
</Callout>

### 4. Pick a room

Pick a room from `PractitionerRole.location`, and read it back to confirm it is active.

<Endpoint method="GET" path="/fhir/R4/Location/01965e2a-8c4d-7000-9020-0000000000a1" />

```bash
curl "https://api.huli.ai/fhir/R4/Location/01965e2a-8c4d-7000-9020-0000000000a1" \
  -H "Authorization: Bearer $HULI_TOKEN" \
  -H "Accept: application/fhir+json"
```

```json
{
  "resourceType": "Location",
  "id": "01965e2a-8c4d-7000-9020-0000000000a1",
  "status": "active",
  "name": "Consultorio 1",
  "managingOrganization": {
    "reference": "Organization/01965e2a-8c4d-7000-9001-0000000000b0"
  }
}
```

Because this room came from the practitioner's own `PractitionerRole.location`, it is
guaranteed to be inside their assigned-location set — which is the set the booking guard
checks. A room taken only from `HealthcareService.location` is not guaranteed to be, and can
be rejected at write time.

### 5. Find a free slot

Slots are computed on the fly from the schedulable resource's schedules minus its booked
appointments, so every slot you get back has `status: "free"`. Search either by `schedule` or
directly by `actor` (the `PractitionerRole.id` from step 3). The `start`/`end` window is
capped at 31 days.

To find the schedule first:

<Endpoint method="GET" path="/fhir/R4/Schedule?actor=01965e2a-8c4d-7000-9030-0000000000b1" />

Or skip straight to slots by actor:

<Endpoint method="GET" path="/fhir/R4/Slot?actor=01965e2a-8c4d-7000-9030-0000000000b1" />

:::CodeGroup

```bash
curl "https://api.huli.ai/fhir/R4/Slot?actor=01965e2a-8c4d-7000-9030-0000000000b1" \
  -H "Authorization: Bearer $HULI_TOKEN" \
  -H "Accept: application/fhir+json"
```

```typescript
const params = new URLSearchParams();
params.set('actor', '01965e2a-8c4d-7000-9030-0000000000b1');

const resp = await fetch(`https://api.huli.ai/fhir/R4/Slot?${params}`, {
  headers: {
    Authorization: `Bearer ${process.env.HULI_TOKEN}`,
    Accept: 'application/fhir+json',
  },
});

const bundle = await resp.json();
const slot = bundle.entry?.[0]?.resource;
// Carry slot.start and slot.end into the Appointment you POST next.
console.log(slot?.start, slot?.end);
```

```python
import os
import requests

resp = requests.get(
    "https://api.huli.ai/fhir/R4/Slot",
    params={
        "actor": "01965e2a-8c4d-7000-9030-0000000000b1",
    },
    headers={
        "Authorization": f"Bearer {os.environ['HULI_TOKEN']}",
        "Accept": "application/fhir+json",
    },
    timeout=30,
)
bundle = resp.json()
slot = bundle["entry"][0]["resource"]
# Carry slot["start"] and slot["end"] into the Appointment you POST next.
print(slot["start"], slot["end"])
```

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class FindSlot {
    public static void main(String[] args) throws Exception {
        String query = "actor=01965e2a-8c4d-7000-9030-0000000000b1";

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.huli.ai/fhir/R4/Slot?" + query))
            .header("Authorization", "Bearer " + System.getenv("HULI_TOKEN"))
            .header("Accept", "application/fhir+json")
            .GET()
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        // entry[0].resource.start / .end feed the Appointment you POST next.
        System.out.println(response.body());
    }
}
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
)

func main() {
	endpoint, err := url.Parse("https://api.huli.ai/fhir/R4/Slot")
	if err != nil {
		panic(err)
	}
	q := endpoint.Query()
	q.Set("actor", "01965e2a-8c4d-7000-9030-0000000000b1")
	endpoint.RawQuery = q.Encode()

	req, err := http.NewRequest(http.MethodGet, endpoint.String(), nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("HULI_TOKEN"))
	req.Header.Set("Accept", "application/fhir+json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	// entry[0].resource.start / .end feed the Appointment you POST next.
	fmt.Printf("%s\n", body)
}
```

:::

<RunnableRequest method="GET" path="/fhir/R4/Slot?actor={{practitionerRoleId}}" recipe="booking-an-appointment" captures='[{"name":"slotStart","path":"entry.0.resource.start"},{"name":"slotEnd","path":"entry.0.resource.end"}]' />

<Callout variant="note">
The Run button above sends no `start`/`end`, so the API applies its default window — now through
the next 7 days — which is what most integrations want. Every sandbox practitioner is seeded with
a Mon–Fri 09:00–17:00 schedule valid for a year, so a fresh sandbox always returns free slots
here. If you pass explicit dates instead, keep the span at 31 days or less (the cap above) and in
the schedule's validity window.
</Callout>

A `Slot` inside the `searchset`:

```json
{
  "resourceType": "Slot",
  "id": "01965e2a-8c4d-7000-9040-0000000000c2",
  "schedule": {
    "reference": "Schedule/01965e2a-8c4d-7000-9035-0000000000d3"
  },
  "status": "free",
  "start": "2026-06-17T09:00:00.000-06:00",
  "end": "2026-06-17T09:30:00.000-06:00"
}
```

Carry that slot's `start` and `end` into the booking.

### 6. POST the Appointment

Assemble the four discovered values into the create body: the `serviceType` coding from step
2 (verbatim), the slot's `start`/`end` from step 5, and a `participant` array naming the
practitioner and the room `Location`. Add the patient participant for a patient-facing
booking.

The body below is the **comprehensive** form — every field the create decoder honors on an
Appointment write. Required fields are flagged inline. The **Full field reference** after the
example is precise about which fields the create decoder reads into
the stored appointment versus which are server-derived from the chosen service — read it before
assuming a field round-trips. A minimal write needs `status`, `start`, `end`, a `serviceType`,
at least one practitioner participant, and a room `Location` participant — plus a `specialty`
selection when the chosen service is multi-specialty (the example includes one).

<Endpoint method="POST" path="/fhir/R4/Appointment" />

:::CodeGroup

```bash
curl -i -X POST https://api.huli.ai/fhir/R4/Appointment \
  -H "Authorization: Bearer $HULI_TOKEN" \
  -H "Content-Type: application/fhir+json" \
  -H "Accept: application/fhir+json" \
  -d '{
    "resourceType": "Appointment",
    "status": "booked",
    "priority": 5,
    "description": "Consulta general — control",
    "patientInstruction": "Llegar 10 minutos antes y traer estudios previos.",
    "serviceType": [
      {
        "coding": [
          {
            "system": "https://fhir.huli.ai/r4/CodeSystem/org-service",
            "code": "01965e2a-8c4d-7000-9010-0000000000f1",
            "display": "Consulta general"
          }
        ]
      }
    ],
    "specialty": [
      {
        "coding": [
          {
            "system": "https://fhir.huli.ai/r4/CodeSystem/specialty",
            "code": "01965e2a-8c4d-7000-9011-0000000000d1",
            "display": "Medicina general"
          }
        ]
      }
    ],
    "start": "2026-06-17T09:00:00.000-06:00",
    "end": "2026-06-17T09:30:00.000-06:00",
    "participant": [
      {
        "actor": { "reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2" },
        "status": "accepted"
      },
      {
        "actor": { "reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1" },
        "status": "accepted"
      },
      {
        "actor": { "reference": "Location/01965e2a-8c4d-7000-9020-0000000000a1" },
        "status": "accepted"
      }
    ],
    "extension": [
      {
        "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-insurance-snapshot",
        "extension": [
          { "url": "provider", "valueString": "Seguros Monterrey" },
          { "url": "policyNumber", "valueString": "POL-99812" },
          { "url": "certificateNumber", "valueString": "CERT-44120" }
        ]
      }
    ]
  }'
```

```typescript
const serviceType = {
  coding: [
    {
      system: 'https://fhir.huli.ai/r4/CodeSystem/org-service',
      code: '01965e2a-8c4d-7000-9010-0000000000f1',
      display: 'Consulta general',
    },
  ],
};

const appointment = {
  resourceType: 'Appointment',
  status: 'booked', // required
  priority: 5, // optional — uint; the decoder stores it verbatim
  description: 'Consulta general — control', // optional — stored
  patientInstruction: 'Llegar 10 minutos antes y traer estudios previos.', // optional — stored
  serviceType: [serviceType], // required — verbatim from HealthcareService.type; resolves to the org service
  specialty: [
    // required ONLY when the chosen service offers ≥2 specialties; the bookable
    // coding is verbatim from HealthcareService.specialty[].coding (specialty CodeSystem).
    // A single-/all-specialty service derives it server-side — omit it then.
    {
      coding: [
        {
          system: 'https://fhir.huli.ai/r4/CodeSystem/specialty',
          code: '01965e2a-8c4d-7000-9011-0000000000d1',
          display: 'Medicina general',
        },
      ],
    },
  ],
  start: '2026-06-17T09:00:00.000-06:00', // required — from the free slot
  end: '2026-06-17T09:30:00.000-06:00', // required
  participant: [
    // All three actors are decoded and persisted as the appointment's participants:
    // the patient (optional), the practitioner (≥1 required), and the room Location (required).
    // Equipment is optional via a Device/<id> actor.
    { actor: { reference: 'Patient/01965e2a-8c4d-7000-9001-0000000000a2' }, status: 'accepted' },
    {
      actor: { reference: 'Practitioner/01965e2a-8c4d-7000-9001-0000000000c1' },
      status: 'accepted',
    },
    { actor: { reference: 'Location/01965e2a-8c4d-7000-9020-0000000000a1' }, status: 'accepted' },
  ],
  extension: [
    {
      // insurance snapshot — provider required within the block; policy/certificate optional
      url: 'https://fhir.huli.ai/r4/StructureDefinition/huli-insurance-snapshot',
      extension: [
        { url: 'provider', valueString: 'Seguros Monterrey' },
        { url: 'policyNumber', valueString: 'POL-99812' },
        { url: 'certificateNumber', valueString: 'CERT-44120' },
      ],
    },
  ],
};

const resp = await fetch('https://api.huli.ai/fhir/R4/Appointment', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.HULI_TOKEN}`,
    'Content-Type': 'application/fhir+json',
    Accept: 'application/fhir+json',
  },
  body: JSON.stringify(appointment),
});

if (resp.status === 201) {
  const created = (await resp.json()) as { id: string };
  console.log('booked', created.id);
} else {
  const outcome = (await resp.json()) as { issue: { diagnostics: string }[] };
  // The HP-/HPB- code is the prefix of issue[0].diagnostics — split on ': '.
  const [code] = outcome.issue[0].diagnostics.split(': ', 1);
  console.log(resp.status, code, outcome.issue[0].diagnostics);
}
```

```python
import os
import requests

service_type = {
    "coding": [
        {
            "system": "https://fhir.huli.ai/r4/CodeSystem/org-service",
            "code": "01965e2a-8c4d-7000-9010-0000000000f1",
            "display": "Consulta general",
        }
    ]
}

appointment = {
    "resourceType": "Appointment",
    "status": "booked",  # required
    "priority": 5,  # optional — uint; stored verbatim
    "description": "Consulta general — control",  # optional — stored
    "patientInstruction": "Llegar 10 minutos antes y traer estudios previos.",  # optional — stored
    "serviceType": [service_type],  # required — verbatim from HealthcareService.type; resolves to the org service
    "specialty": [
        # required ONLY when the chosen service offers ≥2 specialties; the bookable
        # coding is verbatim from HealthcareService.specialty[].coding (specialty CodeSystem).
        # A single-/all-specialty service derives it server-side — omit it then.
        {
            "coding": [
                {
                    "system": "https://fhir.huli.ai/r4/CodeSystem/specialty",
                    "code": "01965e2a-8c4d-7000-9011-0000000000d1",
                    "display": "Medicina general",
                }
            ]
        }
    ],
    "start": "2026-06-17T09:00:00.000-06:00",  # required — from the free slot
    "end": "2026-06-17T09:30:00.000-06:00",  # required
    "participant": [
        # All three actors are decoded and persisted as the appointment's participants:
        # the patient (optional), the practitioner (≥1 required), and the room Location (required).
        # Equipment is optional via a Device/<id> actor.
        {"actor": {"reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2"}, "status": "accepted"},
        {"actor": {"reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1"}, "status": "accepted"},
        {"actor": {"reference": "Location/01965e2a-8c4d-7000-9020-0000000000a1"}, "status": "accepted"},
    ],
    "extension": [
        {
            # insurance snapshot — provider required within the block; policy/certificate optional
            "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-insurance-snapshot",
            "extension": [
                {"url": "provider", "valueString": "Seguros Monterrey"},
                {"url": "policyNumber", "valueString": "POL-99812"},
                {"url": "certificateNumber", "valueString": "CERT-44120"},
            ],
        }
    ],
}

resp = requests.post(
    "https://api.huli.ai/fhir/R4/Appointment",
    headers={
        "Authorization": f"Bearer {os.environ['HULI_TOKEN']}",
        "Content-Type": "application/fhir+json",
        "Accept": "application/fhir+json",
    },
    json=appointment,
    timeout=30,
)

if resp.status_code == 201:
    print("booked", resp.json()["id"])
else:
    outcome = resp.json()
    # The HP-/HPB- code is the prefix of issue[0].diagnostics — split on ": ".
    code = outcome["issue"][0]["diagnostics"].split(": ", 1)[0]
    print(resp.status_code, code, outcome["issue"][0]["diagnostics"])
```

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class BookAppointment {
    public static void main(String[] args) throws Exception {
        // serviceType is the org-service coding from HealthcareService, verbatim.
        // start/end come from the free slot. Hand-built JSON keeps this
        // dependency-free; a real client would use a JSON library.
        // status/start/end and serviceType are required; priority/description/
        // patientInstruction and the insurance-snapshot extension are optional
        // fields the decoder stores. The patient, practitioner (≥1 required) and room
        // Location (required) participants are all decoded and persisted.
        String appointment = "{"
            + "\"resourceType\":\"Appointment\","
            + "\"status\":\"booked\","
            + "\"priority\":5,"
            + "\"description\":\"Consulta general — control\","
            + "\"patientInstruction\":\"Llegar 10 minutos antes y traer estudios previos.\","
            + "\"serviceType\":[{\"coding\":[{"
            + "\"system\":\"https://fhir.huli.ai/r4/CodeSystem/org-service\","
            + "\"code\":\"01965e2a-8c4d-7000-9010-0000000000f1\","
            + "\"display\":\"Consulta general\"}]}],"
            // specialty is required only for a multi-specialty service.
            + "\"specialty\":[{\"coding\":[{"
            + "\"system\":\"https://fhir.huli.ai/r4/CodeSystem/specialty\","
            + "\"code\":\"01965e2a-8c4d-7000-9011-0000000000d1\","
            + "\"display\":\"Medicina general\"}]}],"
            + "\"start\":\"2026-06-17T09:00:00.000-06:00\","
            + "\"end\":\"2026-06-17T09:30:00.000-06:00\","
            + "\"participant\":["
            + "{\"actor\":{\"reference\":\"Patient/01965e2a-8c4d-7000-9001-0000000000a2\"},\"status\":\"accepted\"},"
            + "{\"actor\":{\"reference\":\"Practitioner/01965e2a-8c4d-7000-9001-0000000000c1\"},\"status\":\"accepted\"},"
            + "{\"actor\":{\"reference\":\"Location/01965e2a-8c4d-7000-9020-0000000000a1\"},\"status\":\"accepted\"}"
            + "],"
            + "\"extension\":[{"
            + "\"url\":\"https://fhir.huli.ai/r4/StructureDefinition/huli-insurance-snapshot\","
            + "\"extension\":["
            + "{\"url\":\"provider\",\"valueString\":\"Seguros Monterrey\"},"
            + "{\"url\":\"policyNumber\",\"valueString\":\"POL-99812\"},"
            + "{\"url\":\"certificateNumber\",\"valueString\":\"CERT-44120\"}"
            + "]}]}";

        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.huli.ai/fhir/R4/Appointment"))
            .header("Authorization", "Bearer " + System.getenv("HULI_TOKEN"))
            .header("Content-Type", "application/fhir+json")
            .header("Accept", "application/fhir+json")
            .POST(HttpRequest.BodyPublishers.ofString(appointment))
            .build();

        HttpResponse<String> response =
            client.send(request, HttpResponse.BodyHandlers.ofString());

        switch (response.statusCode()) {
            case 201 -> System.out.println("201 booked\n" + response.body());
            case 400 -> // HPB-00101 structural validation
                System.out.println("400 validation\n" + response.body());
            case 409 -> // HP-00803 the slot is no longer free
                System.out.println("409 conflict\n" + response.body());
            case 422 -> // HP-008xx booking precondition
                System.out.println("422 precondition\n" + response.body());
            default -> System.out.println(response.statusCode() + "\n" + response.body());
        }
    }
}
```

```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	// serviceType is the org-service coding from HealthcareService, verbatim, and is
	// required; start/end come from the free slot. priority/description/
	// patientInstruction and the insurance-snapshot extension are optional
	// fields the decoder stores. The patient, practitioner (≥1 required) and room
	// Location (required) participants are all decoded and persisted.
	body := []byte(`{
		"resourceType": "Appointment",
		"status": "booked",
		"priority": 5,
		"description": "Consulta general — control",
		"patientInstruction": "Llegar 10 minutos antes y traer estudios previos.",
		"serviceType": [{"coding": [{
			"system": "https://fhir.huli.ai/r4/CodeSystem/org-service",
			"code": "01965e2a-8c4d-7000-9010-0000000000f1",
			"display": "Consulta general"
		}]}],
		"specialty": [{"coding": [{
			"system": "https://fhir.huli.ai/r4/CodeSystem/specialty",
			"code": "01965e2a-8c4d-7000-9011-0000000000d1",
			"display": "Medicina general"
		}]}],
		"start": "2026-06-17T09:00:00.000-06:00",
		"end": "2026-06-17T09:30:00.000-06:00",
		"participant": [
			{"actor": {"reference": "Patient/01965e2a-8c4d-7000-9001-0000000000a2"}, "status": "accepted"},
			{"actor": {"reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1"}, "status": "accepted"},
			{"actor": {"reference": "Location/01965e2a-8c4d-7000-9020-0000000000a1"}, "status": "accepted"}
		],
		"extension": [{
			"url": "https://fhir.huli.ai/r4/StructureDefinition/huli-insurance-snapshot",
			"extension": [
				{"url": "provider", "valueString": "Seguros Monterrey"},
				{"url": "policyNumber", "valueString": "POL-99812"},
				{"url": "certificateNumber", "valueString": "CERT-44120"}
			]
		}]
	}`)

	req, err := http.NewRequest(http.MethodPost,
		"https://api.huli.ai/fhir/R4/Appointment", bytes.NewReader(body))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("HULI_TOKEN"))
	req.Header.Set("Content-Type", "application/fhir+json")
	req.Header.Set("Accept", "application/fhir+json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}

	switch resp.StatusCode {
	case http.StatusCreated:
		fmt.Printf("201 booked\n%s\n", out)
	case http.StatusBadRequest: // HPB-00101 structural validation
		fmt.Printf("400 validation\n%s\n", out)
	case http.StatusConflict: // HP-00803 the slot is no longer free
		fmt.Printf("409 conflict\n%s\n", out)
	case http.StatusUnprocessableEntity: // HP-008xx booking precondition
		fmt.Printf("422 precondition\n%s\n", out)
	default:
		fmt.Printf("%d\n%s\n", resp.StatusCode, out)
	}
}
```

:::

<RunnableRequest method="POST" path="/fhir/R4/Appointment" recipe="booking-an-appointment" body='{"resourceType":"Appointment","status":"booked","serviceType":[{"coding":[{"system":"https://fhir.huli.ai/r4/CodeSystem/org-service","code":"{{serviceTypeCode}}"}]}],"start":"{{slotStart}}","end":"{{slotEnd}}","participant":[{"actor":{"reference":"Practitioner/{{practitionerRoleId}}"},"status":"accepted"},{"actor":{"reference":"{{roomLocationRef}}"},"status":"accepted"}]}' captures='[{"name":"appointmentId","path":"id"}]' />

<Callout variant="note">
The Run button above sends the **minimal** write — no patient participant, no `specialty` (only
required when the discovered service offers two or more specialties; see the full field
reference below). Chained from the steps above: `serviceTypeCode` (step 2), `practitionerRoleId`
+ `roomLocationRef` (step 3), `slotStart`/`slotEnd` (step 5).
</Callout>

A `201 Created` returns the stored `Appointment` with a server-assigned `id`. Set `status` to
`proposed` instead of `booked` if your flow needs an intermediate "requested, awaiting
confirmation" state before it firms up.

#### Full field reference

Every field the Appointment write surface touches. The public write routes through the same
scheduling service the in-app calendar uses, so the request
body must name the resources an appointment needs; the service then derives the appointment
type, specialty, and booking policy from the chosen service. Required: `status`, `start`, `end`,
`serviceType`, at least one practitioner participant, and a room `Location` participant — plus
`specialty` when the chosen service offers two or more specialties.

| Field                                        | Req?              | Honored on write | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| -------------------------------------------- | ----------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`                                     | **required**      | yes              | A valid FHIR appointment status; maps to the Huli status id.                                                                                                                                                                                                                                                                                                                                                                                                     |
| `start`                                      | **required**      | yes              | Slot start (from step 5).                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `end`                                        | **required**      | yes              | Slot end.                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `serviceType[].coding`                       | **required**      | yes              | The `org-service` coding from `HealthcareService.type` (system `…/CodeSystem/org-service`, `code` = the org service UUID). Resolves to the org service; the service derives appointment type, specialty, booking policy, and per-service resource requirements. Missing or unresolvable → `422`.                                                                                                                                                                 |
| `participant[].actor` `Practitioner/<uuid>`  | **required** (≥1) | yes              | Decoded and persisted as the appointment's participants. At least one practitioner is required.                                                                                                                                                                                                                                                                                                                                                                  |
| `participant[].actor` `Location/<uuid>`      | **required**      | yes              | The room. Decoded and persisted; missing → `400` (`HP-00816`).                                                                                                                                                                                                                                                                                                                                                                                                   |
| `participant[].actor` `Patient/<uuid>`       | optional          | yes              | The patient participant (omit for walk-ins / admin meetings).                                                                                                                                                                                                                                                                                                                                                                                                    |
| `participant[].actor` `Device/<uuid>`        | optional          | yes              | Equipment participant; decoded and persisted when the service requires it.                                                                                                                                                                                                                                                                                                                                                                                       |
| `priority`                                   | optional          | yes              | Unsigned integer; stored verbatim (`0` = routine).                                                                                                                                                                                                                                                                                                                                                                                                               |
| `description`                                | optional          | yes              | Reason / short label, stored on the appointment.                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `patientInstruction`                         | optional          | yes              | Instructions shown to the patient.                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `extension[]` `…/huli-insurance-snapshot`    | optional          | yes              | Insurance snapshot: nested `provider` (required within the block), `policyNumber`, `certificateNumber`.                                                                                                                                                                                                                                                                                                                                                          |
| `cancelationReason`                          | optional          | on cancel only   | Resolved against the org's cancellation reasons on `PUT status=cancelled`; not read on create.                                                                                                                                                                                                                                                                                                                                                                   |
| `appointmentType`                            | optional          | server-derived   | Read-only on write — derived from the chosen `serviceType` and re-emitted on read.                                                                                                                                                                                                                                                                                                                                                                               |
| `specialty[].coding`                         | conditional       | yes              | The bookable `specialty` coding (system `…/CodeSystem/specialty`, `code` = the specialty UUID) copied verbatim from `HealthcareService.specialty`. **Required** when the chosen service offers ≥2 specialties — omitted → `422` (`HPB-00115`); a specialty the service does not offer → `422` (`HPB-00116`); a non-UUID code → `422` (`value`). For a single-/all-specialty service it is optional (derived server-side; a human/SNOMED-only coding is ignored). |
| `extension[]` `…/confirmation-status`        | optional          | server-derived   | Read-only on write — tracks the patient-confirmation workflow; re-emitted on read.                                                                                                                                                                                                                                                                                                                                                                               |
| `participant[].status` / `required` / `type` | optional          | server-stamped   | The read endpoint stamps these from the stored participant rows; input is not used.                                                                                                                                                                                                                                                                                                                                                                              |

## What to verify

- HTTP status is `201`. <StatusBadge code="201" />
- The response body's `resourceType` is `Appointment` and it carries a server-assigned `id`.
- The `serviceType.coding[0].code` you sent round-trips unchanged — proof the org-service code
  was accepted, not silently dropped.
- `start`/`end` match the slot you chose, and the practitioner + room participants are present.
- Re-search `GET /fhir/R4/Slot?actor=…` for the same window: the slot you booked is no longer
  in the free list.

## What can go wrong

All errors return a FHIR `OperationOutcome`, never a bare string — `{severity, code,
diagnostics}`, with no `details` object. Branch on the HTTP status and `issue[0].code` (the
FHIR IssueType); the Huli code is the prefix of `issue[0].diagnostics`, split on `": "` to
extract it. Structural problems (a missing required field, malformed JSON) surface as
`HPB-00101`; the booking preconditions surface the practice-layer `HP-008xx` codes inside the
same `diagnostics`.

<StatusBadge code="422" /> **`serviceType` missing or unresolvable.** The booking must carry a
`serviceType` whose `coding` uses the `org-service` system with a `code` that is a current org
service UUID. Absent → a `required` issue; present but the wrong system or an unparseable code →
a `value` issue. A bare name string is rejected. This is why step 2 lifts the coding verbatim
from `HealthcareService.type` rather than constructing one. (The old behavior — silently
defaulting to the org's first active service — has been removed.)

<StatusBadge code="400" /> `HP-00807` — **no resource participant.** The booking needs at least
one `participant.actor` referencing a `Practitioner`. Resolve the practitioner in step 3 before
you build the participant array.

<StatusBadge code="400" /> `HP-00816` — **room/location missing.** The booking needs a room:
a `participant.actor` referencing a `Location`. Take it from `PractitionerRole.location` in
step 4.

<StatusBadge code="422" /> `HP-00817` — **the practitioner has no assigned locations.** Every
practitioner must have at least one assigned room to be bookable. If `PractitionerRole.location`
is empty, the practitioner cannot be booked until a room is assigned in HuliPractice.

<StatusBadge code="422" /> `HP-00818` — **the room is not in the practitioners' assigned
locations.** The room you sent must be inside the participating practitioner's assigned set.
Sourcing the room from that practitioner's own `PractitionerRole.location` (step 4) avoids
this — a room taken only from `HealthcareService.location` can fall outside it.

<StatusBadge code="422" /> `HPB-00115` — **a specialty must be selected.** The chosen service
offers two or more specialties (its `HealthcareService.specialty` has ≥2 entries), so the booking
must name which one in `Appointment.specialty` — the API will not guess. Copy one of the service's
`specialty[].coding` entries (the `specialty`-CodeSystem coding) verbatim into the Appointment.

<StatusBadge code="422" /> `HPB-00116` — **specialty not offered by this service.** The
`specialty` you sent is not among the ones the service advertises. Pick a coding straight from the
service's `HealthcareService.specialty` list rather than constructing one. (A present-but-malformed
specialty `code` — a non-UUID — is rejected `422` with a `value` issue instead.)

<StatusBadge code="422" /> `HP-00819` — **practitioner specialty mismatch.** When the service
carries a `specialty` (step 2), every participating practitioner must carry the booked specialty
in their `PractitionerRole.specialty`. Cross-check the role's specialty against the service's
before you book.

<StatusBadge code="409" /> `HP-00803` — **the slot is no longer free.** Between your slot
search and your `POST`, someone else booked it (or it overlaps an existing appointment).
Re-run the step 5 slot search and pick another free slot; do not blindly retry the same body.

<Callout variant="note">
To cancel a booking later, `PUT` the `Appointment` with `status: "cancelled"` and a
`cancelationReason` whose code comes from the cancellation-reason ValueSet. Expand it with
`GET /fhir/R4/ValueSet/$expand?url=https://fhir.huli.ai/r4/ValueSet/cancellation-reason` and
pick a code from the returned `expansion.contains[]` — a free-text reason without a valid code
is rejected. A cancel is **blocked `409` (`HP-00812`)** when a clinical encounter is already
linked to the appointment. To mark a booking as a data-entry mistake instead, `PUT`
`status: "entered-in-error"` — that path takes no reason and runs no cancel guards.
</Callout>

A representative `422` precondition body:

```json
{
  "resourceType": "OperationOutcome",
  "issue": [
    {
      "severity": "error",
      "code": "processing",
      "diagnostics": "HP-00818: Room's location is not among the practitioners' assigned locations"
    }
  ]
}
```

## Next recipes

- **[Scheduling an administrative meeting](/v1/recipes/scheduling-an-administrative-meeting)** —
  book an internal meeting with no patient, a title, and external email invitees, against a
  service whose appointment type is `administrative`.
- **[Wiring a read-only partner](/v1/recipes/wiring-a-read-only-partner)** — consume the
  Appointment + Encounter feed read-only once bookings exist, resolving the
  Practitioner/Organization references they point at.
- **[Run your first authenticated Patient search](/v1/recipes/getting-started-patient-search)**
  — resolve the patient you book for by name or identifier first.
- **Authenticate as a SMART Backend Service** — swap the admin bearer token for
  `client_credentials` + `private_key_jwt` (RS384, 5-minute access token) when you ship the
  booking flow server-to-server.
