---
title: "Book an appointment"
description: "Book an Agenda appointment for a patient with curl, from finding the service and a slot to confirming it, then reschedule and cancel it."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.getarbol.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Book an appointment

This guide books an appointment the way an AI agent or a patient-facing app does, through Agenda's booking operations: find the service, check what the clinic requires, find a slot, book it once the patient confirms, and confirm it to the patient. Then it moves the appointment and cancels it. [How Agenda works](/en/developers/agenda/) explains the model, and [Availability and booking](/en/developers/agenda/availability-and-booking/) every field used here.

You need an API key for your organization with `agenda:bookings:write`, which covers every booking operation, reads included, and `agenda:waitlist:write` to put patients on the waitlist (see [Authentication](/en/developers/authentication/)). The examples read the key from `ARBOL_API_KEY`. Every id, token and patient detail in them is an example.

## Book it

1. **Find the service.** Search for the name the patient used with [`agenda.bookingOfferings.list`](/api/agenda/booking-offerings/list/):

   ```bash
   curl --get https://api.getarbol.com/v1/agenda/booking-offerings \
     --data-urlencode "q=neurología" --data-urlencode "limit=3" \
     --header "Authorization: Bearer $ARBOL_API_KEY"
   ```

   Each offering in `data.data` carries its `id`, its `serviceId`, its sites and what the service asks of the patient: `service.bookingChecklist` to check against what you know, `service.patientInstructions`, and `service.eligibility`, such as `{ "minAgeYears": 18, "maxAgeYears": null }`. With an age rule, you'll need the patient's date of birth.

2. **Check the requirements.** [`agenda.bookingRequirements.get`](/api/agenda/booking-requirements/get/) answers the intake fields, document types and payers; `payerQuery` leaves out unrelated payers:

   ```bash
   curl --get https://api.getarbol.com/v1/agenda/booking-requirements \
     --data-urlencode "payerQuery=Example Health Plan" \
     --header "Authorization: Bearer $ARBOL_API_KEY"
   ```

   In `data.fields`, built-in fields such as `phone` and `payer` are `off`, `optional` or `required`, and `document` is `optional` or `required`; `data.customFields` adds the organization's own; `data.documentTypes` lists the document codes, such as `CC`; and each payer in `data.payers` has a `bookingPolicy`, `open` or `staff_only`. Collect each required value you don't have yet. If the payer's policy is `staff_only`, don't book: [put the patient on the waitlist](#put-the-patient-on-the-waitlist).

3. **Find a slot.** [`agenda.nearestBookingSlots.list`](/api/agenda/nearest-booking-slots/list/) answers the earliest times across every agenda that offers the service:

   ```bash
   curl --get https://api.getarbol.com/v1/agenda/nearest-booking-slots \
     --data-urlencode "serviceId=svc_00000000-0000-4000-8000-000000000001" \
     --data-urlencode "timezone=America/Bogota" --data-urlencode "limit=3" \
     --header "Authorization: Bearer $ARBOL_API_KEY"
   ```

   ```json
   {
     "startsAt": "2026-11-05T09:00:00-05:00",
     "timezone": "America/Bogota",
     "slotToken": "EXAMPLE_SLOT_TOKEN_0001"
   }
   ```

   That's the first slot in `data.data`, abridged: each also has its `endsAt`, `offeringId` and `resourceIds`. Offer the times to the patient in the slot's `timezone`. An empty list means there's no opening as far ahead as the service can be booked: offer the waitlist.

4. **Book it once the patient confirms the time.** Send the `slotToken` unchanged to [`agenda.bookings.create`](/api/agenda/bookings/create/), with the patient and the intake the clinic requires:

   ```bash
   curl https://api.getarbol.com/v1/agenda/bookings \
     --request POST \
     --header "Authorization: Bearer $ARBOL_API_KEY" \
     --header "Content-Type: application/json" \
     --data '{
       "slotToken": "EXAMPLE_SLOT_TOKEN_0001",
       "patient": { "name": "Example Patient", "documentNumber": "1234567890", "documentType": "CC" },
       "patientBirthDate": "1990-01-01",
       "intake": { "phone": "+57 300 000 0000", "entity": "Example Health Plan" }
     }'
   ```

   The answer is the appointment; keep its `id`, which rescheduling and cancelling take. Abridged, from `data`:

   ```json
   {
     "id": "eng_00000000-0000-4000-8000-000000000005",
     "startsAt": "2026-11-05T09:00:00-05:00",
     "guidance": {
       "attendedBy": ["Dr. Example"],
       "patientInstructions": "Bring your medical order and arrive 15 minutes early.",
       "location": { "name": "Example Clinic North", "address": "Calle 00 # 00-00", "city": "Bogotá" }
     },
     "slip": { "url": "https://example.com/EXAMPLE-slip.pdf", "expiresAt": "2026-10-30T16:00:00Z" }
   }
   ```

5. **Confirm it to the patient.** Tell them the time and what `guidance` says: the instructions, who attends, and the site with its address. Download the slip while its link works, within the hour, and send them the PDF file rather than the link:

   ```bash
   curl --fail --output appointment-slip.pdf "$SLIP_URL"
   ```

   Here `SLIP_URL` holds `data.slip.url`; the link carries no credential. [`agenda.bookings.get`](/api/agenda/bookings/get/) reads the appointment again later, with its guidance and slip. Setting a status such as `confirmed` is the clinic's: staff do it with [`agenda.appointments.changeStatus`](/api/agenda/appointments/change-status/), which accepts only people.

## Handle the common errors

| Status | `error.code` | What to do |
| --- | --- | --- |
| `400` | `INVALID_INPUT`, `INVALID_APPOINTMENT_INTAKE` | Fix the fields in `error.data.issues`: each `path` points at one. |
| `403` | `FORBIDDEN` | The key lacks `agenda:bookings:write`. |
| `409` | `BIRTH_DATE_REQUIRED` | The service has an age rule: ask the patient's date of birth and send `patientBirthDate`. |
| `409` | `AGE_NOT_ELIGIBLE` | The patient's age on that day is outside the service's age rule, so this service can't be booked for them. |
| `409` | `PAYER_STAFF_ONLY` | Only the clinic's team books this payer's patients: put the patient on the waitlist. |
| `409` | `RESOURCE_CAPACITY_EXCEEDED`, `SERVICE_CAPACITY_EXCEEDED`, `SCHEDULE_CHANGED` | The time is no longer free: find another slot and confirm it with the patient. |
| `409` | `INVALID_SLOT_TOKEN` | Find a fresh slot, and send its token exactly as returned. |
| `502`, `504` | `OUTCOME_UNKNOWN`, `UPSTREAM_TIMEOUT` | The appointment may exist: check before you repeat, as in [Handle errors and retries](/en/developers/guides/handle-errors-and-retries/#a-booking-that-timed-out), which also covers `429`. |

### Put the patient on the waitlist

With the patient's consent, [`agenda.waitlist.join`](/api/agenda/waitlist/join/) records the request for the clinic's team, with the reason `payer_review` for a `staff_only` payer or `manual` when no time suits. It never books: tell the patient the clinic's team will contact them.

```bash
curl https://api.getarbol.com/v1/agenda/waitlist/join \
  --request POST \
  --header "Authorization: Bearer $ARBOL_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "reason": "payer_review",
    "serviceId": "svc_00000000-0000-4000-8000-000000000001",
    "patient": { "name": "Example Patient", "documentNumber": "1234567890", "documentType": "CC" },
    "phone": "+57 300 000 0000", "payer": "Example Health Plan"
  }'
```

## Reschedule

[`agenda.bookings.lookup`](/api/agenda/bookings/lookup/) takes the patient's document and answers their appointments in `data.appointments`, newest first. Pick the one the patient means by its `startsAt` and `status`; `found: false` means no patient has that document.

```bash
curl https://api.getarbol.com/v1/agenda/bookings/lookup \
  --request POST \
  --header "Content-Type: application/json" \
  --data '{ "documentNumber": "1234567890", "documentType": "CC" }' \
  --header "Authorization: Bearer $ARBOL_API_KEY"
```

Find a new time for the same service as in step 3, where another professional or room of the service is fine, and have the patient confirm it. Then send its `slotToken` to [`agenda.bookings.reschedule`](/api/agenda/bookings/reschedule/):

```bash
curl https://api.getarbol.com/v1/agenda/bookings/eng_00000000-0000-4000-8000-000000000005/reschedule \
  --request POST \
  --header "Authorization: Bearer $ARBOL_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "slotToken": "EXAMPLE_SLOT_TOKEN_0002" }'
```

It answers the appointment at its new time, with guidance and a slip to send again. A slot of another service is refused with `SERVICE_CHANGED`, and a closed appointment with `APPOINTMENT_NOT_MOVABLE`. Rescheduling isn't idempotent: if the call times out, read the appointment with `agenda.bookings.get` and check its `startsAt` before you send it again.

## Cancel

Once the patient confirms which appointment, send the reason they gave to [`agenda.bookings.cancel`](/api/agenda/bookings/cancel/). The slot is freed and the reason is kept in the appointment's history. Cancelling is idempotent, so repeating it is safe.

```bash
curl https://api.getarbol.com/v1/agenda/bookings/eng_00000000-0000-4000-8000-000000000005/cancel \
  --request POST \
  --header "Authorization: Bearer $ARBOL_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "reason": "The patient can no longer attend." }'
```

Source: https://docs.getarbol.com/en/developers/guides/book-an-appointment/index.mdx
