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: system/Appointment.cru for the write (and the discovery resources gated behind it) and system/Practitioner.rs 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. 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 for provisioning and POST /auth/token for the token exchange — this recipe assumes you already hold one.
  • These two scopes on that token:
    • 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.
    • system/Practitioner.rs — read + search Practitioner andPractitionerRole.
  • curl, or Node, Python, Java, or Go if you prefer a language client.

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

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

2. Discover a bookable service

GET/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.

curl "https://api.huli.ai/fhir/R4/HealthcareService?_count=50" \
  -H "Authorization: Bearer $HULI_TOKEN" \
  -H "Accept: application/fhir+json"
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));
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)
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());
    }
}
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)
}
GET/fhir/R4/HealthcareService?_count=50

Set your sandbox token above to run this request.

A representative HealthcareService entry inside the searchset:

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

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.

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

Set your sandbox token above to run this request.

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

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

Set your sandbox token above to run this request.

A PractitionerRole resource:

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

4. Pick a room

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

GET/fhir/R4/Location/01965e2a-8c4d-7000-9020-0000000000a1
curl "https://api.huli.ai/fhir/R4/Location/01965e2a-8c4d-7000-9020-0000000000a1" \
  -H "Authorization: Bearer $HULI_TOKEN" \
  -H "Accept: application/fhir+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:

GET/fhir/R4/Schedule?actor=01965e2a-8c4d-7000-9030-0000000000b1

Or skip straight to slots by actor:

GET/fhir/R4/Slot?actor=01965e2a-8c4d-7000-9030-0000000000b1
curl "https://api.huli.ai/fhir/R4/Slot?actor=01965e2a-8c4d-7000-9030-0000000000b1" \
  -H "Authorization: Bearer $HULI_TOKEN" \
  -H "Accept: application/fhir+json"
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);
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"])
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());
    }
}
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)
}
GET/fhir/R4/Slot?actor={{practitionerRoleId}}

Set your sandbox token above to run this request.

A Slot inside the searchset:

{
  "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).

POST/fhir/R4/Appointment
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" }
        ]
      }
    ]
  }'
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);
}
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"])
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());
        }
    }
}
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)
	}
}
POST/fhir/R4/Appointment

Set your sandbox token above to run this request.

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.

FieldReq?Honored on writeNotes
statusrequiredyesA valid FHIR appointment status; maps to the Huli status id.
startrequiredyesSlot start (from step 5).
endrequiredyesSlot end.
serviceType[].codingrequiredyesThe 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)yesDecoded and persisted as the appointment's participants. At least one practitioner is required.
participant[].actor Location/<uuid>requiredyesThe room. Decoded and persisted; missing → 400 (HP-00816).
participant[].actor Patient/<uuid>optionalyesThe patient participant (omit for walk-ins / admin meetings).
participant[].actor Device/<uuid>optionalyesEquipment participant; decoded and persisted when the service requires it.
priorityoptionalyesUnsigned integer; stored verbatim (0 = routine).
descriptionoptionalyesReason / short label, stored on the appointment.
patientInstructionoptionalyesInstructions shown to the patient.
extension[] …/huli-insurance-snapshotoptionalyesInsurance snapshot: nested provider (required within the block), policyNumber, certificateNumber.
cancelationReasonoptionalon cancel onlyResolved against the org's cancellation reasons on PUT status=cancelled; not read on create.
appointmentTypeoptionalserver-derivedRead-only on write — derived from the chosen serviceType and re-emitted on read.
specialty[].codingconditionalyesThe 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-statusoptionalserver-derivedRead-only on write — tracks the patient-confirmation workflow; re-emitted on read.
participant[].status / required / typeoptionalserver-stampedThe read endpoint stamps these from the stored participant rows; input is not used.

What to verify

  • HTTP status is 201. 201 Created
  • 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.

422 Unprocessable Entity 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.)

400 Bad Request HP-00807no 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.

400 Bad Request HP-00816room/location missing. The booking needs a room:

a participant.actor referencing a Location. Take it from PractitionerRole.location in step 4.

422 Unprocessable Entity HP-00817the 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.

422 Unprocessable Entity 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.

422 Unprocessable Entity HPB-00115a 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.

422 Unprocessable Entity HPB-00116specialty 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.)

422 Unprocessable Entity HP-00819practitioner 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.

409 Conflict HP-00803the 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.

A representative 422 precondition body:

{
  "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 — 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 — consume the Appointment + Encounter feed read-only once bookings exist, resolving the Practitioner/Organization references they point at.
  • Run your first authenticated 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.