Scheduling an administrative meeting

Book an internal meeting — a staff huddle, a vendor call, a blocked planning hour — as a FHIR R4 Appointment that has no patient. An administrative meeting is an appointment against a service whose appointment type is administrative; it carries a title, may run all day, and may invite external email attendees who are not Huli users. The scopes are the booking ones: system/Appointment.cru for the write and system/Practitioner.rs for the practitioner wiring.

The shape differs from a clinical booking in three load-bearing ways, and the server enforces all three: an administrative meeting requires a title, rejects a patient participant, and may carry external invitees — while a clinical appointment rejects the title and invitees and expects a patient. Pick the right service and the rest follows.

Audience

You build an internal scheduling tool, a calendar sync, or an operations integration that books non-clinical time on a practitioner's calendar. You have already booked a clinical appointment — this recipe reuses that recipe's discovery (service, practitioner, room, slot) and changes only the create body.

You'll need

  • A bearer token from HuliPractice (Practice Settings → Integrations → API Keys), or a SMART Backend Services access token. See Bearer Tokens and POST /auth/token.
  • These two scopes on that token:
    • system/Appointment.cru — create Appointment (and the gated discoveryresources HealthcareService, Location, Schedule, Slot).
    • system/Practitioner.rs — read + search Practitioner and PractitionerRole.
  • A service configured as an administrative meeting type, plus a practitioner and a free slot — discovered exactly as in Booking an appointment end-to-end (steps 2–5 there). The one difference is the service: pick one whose appointment type is administrative. A room (Location) participant is optional for administrative meetings — clinical bookings require one, administrative meetings do not.
  • curl, or Node, Python, Java, or Go.

End state

You hold a 201 Created whose body is the stored Appointment — no patient participant, a huli-appointment-title extension carrying the meeting name, the practitioner participant (plus a room participant if you sent one), and (if you sent them) huli-appointment-external-attendee extensions for the email invitees. It is searchable with appointment-type=administrative.

Steps

1. Export the token

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

2. Find the administrative service and a slot

Discover the bookable values exactly as in Booking an appointment end-to-end: the serviceType coding (from HealthcareService.type), a practitioner and their room (PractitionerRole), and a free Slot. Pick a service whose appointment type is administrative.

To confirm a service is the administrative one, list existing administrative meetings — the appointment-type token filters on the service's derived type:

GET/fhir/R4/Appointment?appointment-type=administrative&_count=20
curl "https://api.huli.ai/fhir/R4/Appointment?appointment-type=administrative&_count=20" \
  -H "Authorization: Bearer $HULI_TOKEN" \
  -H "Accept: application/fhir+json"

Each match carries its serviceType coding and its huli-appointment-title extension — copy the serviceType of one to book another meeting against the same service.

3. POST the administrative Appointment

Assemble the discovered serviceType, the slot's start/end, and the practitioner + room participants — with no Patient participant. Add the required huli-appointment-title extension, an optional huli-appointment-all-day boolean, and one huli-appointment-external-attendee extension per email invitee (each nests a required email and an optional displayName; up to 30).

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",
    "description": "Revisión mensual de operaciones",
    "serviceType": [
      {
        "coding": [
          {
            "system": "https://fhir.huli.ai/r4/CodeSystem/org-service",
            "code": "01965e2a-8c4d-7000-9010-0000000000fa",
            "display": "Reunión administrativa"
          }
        ]
      }
    ],
    "start": "2026-06-18T15:00:00.000-06:00",
    "end": "2026-06-18T16:00:00.000-06:00",
    "participant": [
      {
        "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-appointment-title",
        "valueString": "Revisión mensual de operaciones"
      },
      {
        "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-appointment-all-day",
        "valueBoolean": false
      },
      {
        "url": "https://fhir.huli.ai/r4/StructureDefinition/huli-appointment-external-attendee",
        "extension": [
          { "url": "email", "valueString": "proveedor@ejemplo.com" },
          { "url": "displayName", "valueString": "Proveedor Externo" }
        ]
      }
    ]
  }'
const ext = 'https://fhir.huli.ai/r4/StructureDefinition';

const meeting = {
  resourceType: 'Appointment',
  status: 'booked',
  description: 'Revisión mensual de operaciones',
  serviceType: [
    {
      coding: [
        {
          system: 'https://fhir.huli.ai/r4/CodeSystem/org-service',
          code: '01965e2a-8c4d-7000-9010-0000000000fa', // an administrative-type service
          display: 'Reunión administrativa',
        },
      ],
    },
  ],
  start: '2026-06-18T15:00:00.000-06:00',
  end: '2026-06-18T16:00:00.000-06:00',
  participant: [
    // No Patient participant — an administrative meeting rejects one.
    {
      actor: { reference: 'Practitioner/01965e2a-8c4d-7000-9001-0000000000c1' },
      status: 'accepted',
    },
    { actor: { reference: 'Location/01965e2a-8c4d-7000-9020-0000000000a1' }, status: 'accepted' },
  ],
  extension: [
    { url: `${ext}/huli-appointment-title`, valueString: 'Revisión mensual de operaciones' }, // required
    { url: `${ext}/huli-appointment-all-day`, valueBoolean: false }, // optional
    {
      // optional — repeat per invitee, up to 30
      url: `${ext}/huli-appointment-external-attendee`,
      extension: [
        { url: 'email', valueString: 'proveedor@ejemplo.com' }, // required within the block
        { url: 'displayName', valueString: 'Proveedor Externo' }, // optional
      ],
    },
  ],
};

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(meeting),
});

if (resp.status === 201) {
  console.log('booked', (await resp.json()).id);
} else {
  const outcome = (await resp.json()) as { issue: { diagnostics: string }[] };
  console.log(resp.status, outcome.issue[0].diagnostics);
}
import os
import requests

ext = "https://fhir.huli.ai/r4/StructureDefinition"

meeting = {
    "resourceType": "Appointment",
    "status": "booked",
    "description": "Revisión mensual de operaciones",
    "serviceType": [
        {
            "coding": [
                {
                    "system": "https://fhir.huli.ai/r4/CodeSystem/org-service",
                    "code": "01965e2a-8c4d-7000-9010-0000000000fa",  # an administrative-type service
                    "display": "Reunión administrativa",
                }
            ]
        }
    ],
    "start": "2026-06-18T15:00:00.000-06:00",
    "end": "2026-06-18T16:00:00.000-06:00",
    "participant": [
        # No Patient participant — an administrative meeting rejects one.
        {"actor": {"reference": "Practitioner/01965e2a-8c4d-7000-9001-0000000000c1"}, "status": "accepted"},
        {"actor": {"reference": "Location/01965e2a-8c4d-7000-9020-0000000000a1"}, "status": "accepted"},
    ],
    "extension": [
        {"url": f"{ext}/huli-appointment-title", "valueString": "Revisión mensual de operaciones"},  # required
        {"url": f"{ext}/huli-appointment-all-day", "valueBoolean": False},  # optional
        {
            # optional — repeat per invitee, up to 30
            "url": f"{ext}/huli-appointment-external-attendee",
            "extension": [
                {"url": "email", "valueString": "proveedor@ejemplo.com"},  # required within the block
                {"url": "displayName", "valueString": "Proveedor Externo"},  # optional
            ],
        },
    ],
}

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=meeting,
    timeout=30,
)
print(resp.status_code, resp.json().get("id") or resp.json()["issue"][0]["diagnostics"])
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class AdminMeeting {
    public static void main(String[] args) throws Exception {
        // No Patient participant; the title extension is required; all-day and
        // external-attendee extensions are optional. Hand-built JSON keeps this
        // dependency-free; a real client would use a JSON library.
        String ext = "https://fhir.huli.ai/r4/StructureDefinition/";
        String body = "{"
            + "\"resourceType\":\"Appointment\",\"status\":\"booked\","
            + "\"description\":\"Revisión mensual de operaciones\","
            + "\"serviceType\":[{\"coding\":[{"
            + "\"system\":\"https://fhir.huli.ai/r4/CodeSystem/org-service\","
            + "\"code\":\"01965e2a-8c4d-7000-9010-0000000000fa\"}]}],"
            + "\"start\":\"2026-06-18T15:00:00.000-06:00\","
            + "\"end\":\"2026-06-18T16:00:00.000-06:00\","
            + "\"participant\":["
            + "{\"actor\":{\"reference\":\"Practitioner/01965e2a-8c4d-7000-9001-0000000000c1\"},\"status\":\"accepted\"},"
            + "{\"actor\":{\"reference\":\"Location/01965e2a-8c4d-7000-9020-0000000000a1\"},\"status\":\"accepted\"}],"
            + "\"extension\":["
            + "{\"url\":\"" + ext + "huli-appointment-title\",\"valueString\":\"Revisión mensual de operaciones\"},"
            + "{\"url\":\"" + ext + "huli-appointment-external-attendee\",\"extension\":["
            + "{\"url\":\"email\",\"valueString\":\"proveedor@ejemplo.com\"},"
            + "{\"url\":\"displayName\",\"valueString\":\"Proveedor Externo\"}]}]}";

        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(body))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode());
        System.out.println(response.body());
    }
}
package main

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

func main() {
	// No Patient participant; the title extension is required; all-day and
	// external-attendee extensions are optional.
	body := []byte(`{
		"resourceType": "Appointment",
		"status": "booked",
		"description": "Revisión mensual de operaciones",
		"serviceType": [{"coding": [{
			"system": "https://fhir.huli.ai/r4/CodeSystem/org-service",
			"code": "01965e2a-8c4d-7000-9010-0000000000fa"
		}]}],
		"start": "2026-06-18T15:00:00.000-06:00",
		"end": "2026-06-18T16:00:00.000-06:00",
		"participant": [
			{"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-appointment-title", "valueString": "Revisión mensual de operaciones"},
			{"url": "https://fhir.huli.ai/r4/StructureDefinition/huli-appointment-all-day", "valueBoolean": false},
			{"url": "https://fhir.huli.ai/r4/StructureDefinition/huli-appointment-external-attendee", "extension": [
				{"url": "email", "valueString": "proveedor@ejemplo.com"},
				{"url": "displayName", "valueString": "Proveedor Externo"}
			]}
		]
	}`)

	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)
	}
	fmt.Printf("%d\n%s\n", resp.StatusCode, out)
}

A 201 Created returns the stored meeting. The huli-appointment-title and (when true) huli-appointment-all-day extensions round-trip on every read; the huli-appointment-external-attendee extensions round-trip on a single-resource GET, create, and update — but never on a search, because invitee emails are PII held off the search keyset path.

4. Edit invitees and flip a meeting later

A PUT is a full replace. Supplying huli-appointment-external-attendee extension(s) replaces the invitee set; omitting them leaves it unchanged. Because a PUT replaces everything, you can flip a meeting between administrative and clinical: omit the title (clears it) and add a Patient participant to turn a meeting into a clinical appointment against a clinical service, or the reverse.

What to verify

  • HTTP status is 201. 201 Created The response resourceType is Appointment with a server-assigned id.
  • There is no Patient participant, and the practitioner + room participants are present.
  • The huli-appointment-title extension round-trips with your meeting name.
  • The meeting appears in GET /fhir/R4/Appointment?appointment-type=administrative.
  • A single-resource GET shows the huli-appointment-external-attendee extensions; the same resource in a search result does not.

What can go wrong

All errors return a FHIR OperationOutcome{severity, code, diagnostics}, no details object. Structural problems surface as HPB-00101; the booking preconditions (practitioner, room, slot conflict) surface the practice-layer HP-008xx codes documented in Booking an appointment end-to-end.

400 Bad Request Missing title on an administrative meeting. A meeting against an

administrative service requires huli-appointment-title; omitting it is rejected with issue[0].code required and an expression of Appointment.extension(huli-appointment-title). Add the title extension.

400 Bad Request Title or invitees on a clinical appointment. A

huli-appointment-title or huli-appointment-external-attendee against a clinical service is rejected with a value issue — those fields belong only to administrative meetings. Either drop them or book against an administrative service.

400 Bad Request Patient participant on an administrative meeting. An administrative

meeting must not name a Patient participant. Remove it (administrative meetings are internal — the external-attendee extensions carry guests instead).

422 Unprocessable Entity Booking precondition / specialty. The same guards as a clinical

booking apply: the practitioner must have the room in their assigned locations, the slot must be free (409 HP-00803), and a multi-specialty service still needs an Appointment.specialty selection (HPB-00115/HPB-00116). See the booking recipe's What can go wrong.

Next recipes