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/tokenfor the token exchange — this recipe assumes you already hold one. - These two scopes on that token:
- system/Appointment.cru — create
Appointment(.crualso grants read +search). The discovery resourcesHealthcareService,Location,Schedule, andSlotare gated behind the Appointment scope, so this one grant covers them. - system/Practitioner.rs — read + search
PractitionerandPractitionerRole.
- system/Appointment.cru — create
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
/fhir/R4/HealthcareService?_count=50Each 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)
}
/fhir/R4/HealthcareService?_count=50Set 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.
/fhir/R4/Practitioner?name=Fern%C3%A1ndezcurl "https://api.huli.ai/fhir/R4/Practitioner?name=Fern%C3%A1ndez" \
-H "Authorization: Bearer $HULI_TOKEN" \
-H "Accept: application/fhir+json"
/fhir/R4/Practitioner?_count=1Set your sandbox token above to run this request.
With the practitioner's id in hand, read their role wiring:
/fhir/R4/PractitionerRole?practitioner=01965e2a-8c4d-7000-9001-0000000000c1curl "https://api.huli.ai/fhir/R4/PractitionerRole?practitioner=01965e2a-8c4d-7000-9001-0000000000c1" \
-H "Authorization: Bearer $HULI_TOKEN" \
-H "Accept: application/fhir+json"
/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.
/fhir/R4/Location/01965e2a-8c4d-7000-9020-0000000000a1curl "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:
/fhir/R4/Schedule?actor=01965e2a-8c4d-7000-9030-0000000000b1Or skip straight to slots by actor:
/fhir/R4/Slot?actor=01965e2a-8c4d-7000-9030-0000000000b1curl "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)
}
/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).
/fhir/R4/Appointmentcurl -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)
}
}
/fhir/R4/AppointmentSet 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.
| 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. 201 Created - The response body's
resourceTypeisAppointmentand it carries a server-assignedid. - The
serviceType.coding[0].codeyou sent round-trips unchanged — proof the org-service code was accepted, not silently dropped. start/endmatch 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.
serviceType missing or unresolvable. The booking must carry aserviceType 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.)
HP-00807 — no resource participant. The booking needs at leastone participant.actor referencing a Practitioner. Resolve the practitioner in step 3 before you build the participant array.
HP-00816 — room/location missing. The booking needs a room:a participant.actor referencing a Location. Take it from PractitionerRole.location in step 4.
HP-00817 — the practitioner has no assigned locations. Everypractitioner 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.
HP-00818 — **the room is not in the practitioners' assignedlocations.** 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.
HPB-00115 — a specialty must be selected. The chosen serviceoffers 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.
HPB-00116 — specialty not offered by this service. Thespecialty 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.)
HP-00819 — practitioner specialty mismatch. When the servicecarries 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.
HP-00803 — the slot is no longer free. Between your slotsearch 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.