---
title: Scheduling an administrative meeting
description: Book an internal meeting as a FHIR R4 Appointment with no patient — a required title, optional all-day flag, and external email invitees — against a service whose appointment type is administrative.
nav: Recipes
order: 25
version: v1
source: handwritten
updated: 2026-06-26
---

# 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: <Scope>system/Appointment.cru</Scope> for the write and <Scope>system/Practitioner.rs</Scope>
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](/v1/recipes/booking-an-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](/v1/auth/bearer) and
  [`POST /auth/token`](/v1/auth).
- These two scopes on that token:
  - <Scope name="system/Appointment.cru" /> — create `Appointment` (and the gated discovery
    resources `HealthcareService`, `Location`, `Schedule`, `Slot`).
  - <Scope name="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](/v1/recipes/booking-an-appointment) (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.

<Callout variant="info">
The appointment type is **derived from the service**, not sent by you. To find which service is
administrative, search existing meetings with `GET /fhir/R4/Appointment?appointment-type=administrative`
and read the `serviceType` off one, or confirm with the clinic admin which catalog entry is the
meeting type. A `huli-appointment-title` sent against a *clinical* service is rejected, and a
clinical booking with **no** patient against an *administrative* service is rejected for a missing
title — the two kinds are mutually exclusive.
</Callout>

## 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`.

<Callout variant="info">
**Invitees are emailed automatically.** Creating this meeting emails every
`huli-appointment-external-attendee` a calendar (`ICS`) **REQUEST**; cancelling it sends a
**CANCEL**, and editing it diffs the attendee set (added invitees get a REQUEST, removed ones a
CANCEL, and a time change re-invites the rest). It's the same dispatch the in-app app uses, sent
post-commit and at-least-once — you don't need to send your own invites.
</Callout>

## Steps

### 1. Export the token

```bash
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](/v1/recipes/booking-an-appointment): 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:

<Endpoint method="GET" path="/fhir/R4/Appointment?appointment-type=administrative&_count=20" />

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

<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",
    "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" }
        ]
      }
    ]
  }'
```

```typescript
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);
}
```

```python
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"])
```

```java
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());
    }
}
```

```go
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.

<Callout variant="info">
An **all-day** meeting (`huli-appointment-all-day: true`) must span whole local days — the service
enforces midnight-to-midnight bounds, so set `start`/`end` to the local day boundaries. A partial-
day window with the all-day flag set is rejected.
</Callout>

### 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`. <StatusBadge code="201" /> 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](/v1/recipes/booking-an-appointment).

<StatusBadge code="400" /> **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.

<StatusBadge code="400" /> **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.

<StatusBadge code="400" /> **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).

<StatusBadge code="422" /> **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](/v1/recipes/booking-an-appointment).

## Next recipes

- **[Booking an appointment end-to-end](/v1/recipes/booking-an-appointment)** — the clinical
  counterpart, with the full discovery flow this recipe reuses.
- **[Wiring a read-only partner](/v1/recipes/wiring-a-read-only-partner)** — consume the
  appointment feed read-only, filtering administrative vs clinical with `appointment-type`.
