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

# Managing appointment definitions

> Create and maintain the bookable-service definitions behind appointments: booking type, capacity, opening hours, locations, staff, questionnaires, notifications, and visibility.

An **Appointment Definition** is the bookable service behind every appointment: it sets the duration, [booking type](#booking-type-and-capability-flags), opening hours, the locations and staff it is offered at, capacity, an optional booking questionnaire, notifications, and visibility rules. Create one before customers can book. For the model behind definitions, see the [Appointments concept](/concepts/visits/appointments).

All endpoints are bearer authenticated and live under `https://api.[tenant].getomneo.com/api/v3`.

## Create a definition

Send a `POST` to `/appointment-definitions`. Only `handle`, `name`, `duration_minutes`, and `booking_type` are required. Everything else has a default or can be added later.

```shell theme={null}
curl -X POST "https://api.[tenant].getomneo.com/api/v3/appointment-definitions" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "handle": "bra-fitting",
    "name": "Bra fitting",
    "short_description": "A 30 minute fitting with a specialist.",
    "duration_minutes": 30,
    "booking_type": "approval_required",
    "buffer_before_minutes": 5,
    "buffer_after_minutes": 5,
    "min_lead_minutes": 120,
    "max_advance_days": 30,
    "slot_interval_minutes": 15,
    "allow_customer_booking": true,
    "requires_staff": true,
    "customer_must_select_staff": false,
    "use_staff_from_location": true,
    "max_concurrent_bookings": null,
    "allow_waitlist": true,
    "allow_queue": false,
    "location_ids": [13, 14],
    "is_published": false,
    "is_archived": false
  }'
```

The response is `201` with the saved definition, including its generated `id`. Use that `id` on the nested endpoints below and as `appointment_definition_id` when creating appointments.

### Field reference

| Field                                                  | Type    | Notes                                                                                                         |
| ------------------------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------- |
| `handle`                                               | string  | Required. Unique handle for the definition. Use this, not a slug or key.                                      |
| `name`                                                 | string  | Required. Display name shown in booking flows.                                                                |
| `duration_minutes`                                     | integer | Required. Length of one booking.                                                                              |
| `booking_type`                                         | enum    | Required. `instant`, `approval_required`, or `walk_in_only`. See [below](#booking-type-and-capability-flags). |
| `description`, `short_description`, `long_description` | string  | Copy for the booking flow.                                                                                    |
| `icon`, `image_url`                                    | string  | Display assets for the booking flow.                                                                          |
| `terms_conditions`                                     | string  | Terms shown to the customer at booking.                                                                       |
| `internal_notes`                                       | string  | Operator-only note. Not customer facing.                                                                      |
| `buffer_before_minutes`, `buffer_after_minutes`        | integer | Padding held before and after each booking.                                                                   |
| `min_lead_minutes`                                     | integer | Minimum notice before a slot can be booked.                                                                   |
| `max_advance_days`                                     | integer | How far ahead a slot can be booked.                                                                           |
| `slot_interval_minutes`                                | integer | Spacing between slot start times.                                                                             |
| `meta`                                                 | object  | Custom key/value data.                                                                                        |

`min_lead_minutes`, `max_advance_days`, the buffers, and the opening hours together define which slots the [availability endpoints](/dev-guides/visits/appointments#booking-flow) return.

## Booking type and capability flags

`booking_type` sets how customers book against the definition.

| `booking_type`      | Behaviour                                                                                                              |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `instant`           | The appointment is confirmed the moment it is booked.                                                                  |
| `approval_required` | The customer creates a request; an operator confirms or rejects it.                                                    |
| `walk_in_only`      | No advance booking. Customers join a [walk-in queue](/dev-guides/visits/walk-in-queues-and-waitlists) at the location. |

Independent capability flags layer on top of the booking type.

| Flag                         | Type            | Notes                                                                                                                                           |
| ---------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `allow_customer_booking`     | boolean         | Whether customers can self-book.                                                                                                                |
| `allow_walk_in`              | boolean         | Whether walk-in check-in is allowed.                                                                                                            |
| `requires_staff`             | boolean         | Whether each booking has an assigned staff member.                                                                                              |
| `customer_must_select_staff` | boolean         | Forces the customer to choose a staff member. Set `requires_staff` to `true` as well.                                                           |
| `use_staff_from_location`    | boolean         | Where the eligible staff pool comes from. See [Locations and staff](#locations-and-staff).                                                      |
| `max_concurrent_bookings`    | integer or null | Capacity per slot for non-staff definitions. `null` means unlimited. For staff-required definitions, capacity is one per staff member per slot. |
| `allow_waitlist`             | boolean         | Whether customers can join a [waitlist](/dev-guides/visits/walk-in-queues-and-waitlists).                                                       |
| `allow_queue`                | boolean         | Whether the [walk-in queue](/dev-guides/visits/walk-in-queues-and-waitlists) is available.                                                      |

<Note>Set `max_concurrent_bookings` to `null` for group services with no fixed cap, such as a class or an event. Leave it unset or `null` unless the service has a real per-slot limit.</Note>

## Opening hours

A definition carries two sets of hours, both scoped to the definition itself.

* **Normal hours** are the weekly pattern, one entry per `day_of_week`.
* **Special hours** are dated overrides such as public holidays or a one-off change. A special hour takes precedence over the normal hour when a date matches.

Create either inline on the definition or through the nested endpoints. Inline is convenient at create time; the nested endpoints let you add, update, or remove single entries later without rewriting the whole definition.

Inline on create:

```shell theme={null}
curl -X POST "https://api.[tenant].getomneo.com/api/v3/appointment-definitions" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "handle": "bra-fitting",
    "name": "Bra fitting",
    "duration_minutes": 30,
    "booking_type": "approval_required",
    "normal_hours": [
      { "day_of_week": "MON", "available_from": "09:00", "available_until": "17:00" },
      { "day_of_week": "TUE", "available_from": "09:00", "available_until": "17:00" }
    ],
    "special_hours": [
      {
        "name": "Public holiday",
        "is_repeating": false,
        "available_from": "10:00",
        "available_until": "14:00",
        "start_at": "2026-12-25",
        "end_at": "2026-12-25"
      }
    ]
  }'
```

Add a single normal-hours entry later:

```shell theme={null}
curl -X POST "https://api.[tenant].getomneo.com/api/v3/appointment-definitions/{definitionId}/normal-hours" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{ "day_of_week": "WED", "available_from": "09:00", "available_until": "13:00" }'
```

| Endpoint                                                     | Purpose                                                                                                     |
| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `POST /appointment-definitions/{definitionId}/normal-hours`  | Add one weekly entry (`day_of_week`, `available_from`, `available_until`).                                  |
| `POST /appointment-definitions/{definitionId}/special-hours` | Add one dated override (`name`, `start_at`, `end_at`, `available_from`, `available_until`, `is_repeating`). |

`day_of_week` accepts `MON`, `TUE`, `WED`, `THU`, `FRI`, `SAT`, or `SUN`. Times such as `available_from` are plain local times at the location. Each nested collection also supports `GET`, and each entry supports `PUT` and `DELETE` at `.../normal-hours/{id}` and `.../special-hours/{id}`.

## Locations and staff

A definition is offered at one or more locations and, when it requires staff, draws from a staff pool.

Attach both on create with `location_ids` and `staff_ids`:

```shell theme={null}
curl -X POST "https://api.[tenant].getomneo.com/api/v3/appointment-definitions" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "handle": "styling-session",
    "name": "Styling session",
    "duration_minutes": 45,
    "booking_type": "instant",
    "requires_staff": true,
    "use_staff_from_location": false,
    "location_ids": [13, 14],
    "staff_ids": ["a1b460b2-953f-4587-b4eb-fb0f29b55e02"]
  }'
```

Manage them individually through the nested endpoints:

| Endpoint                                                 | Purpose                                                   |
| -------------------------------------------------------- | --------------------------------------------------------- |
| `POST /appointment-definitions/{definitionId}/locations` | Offer the definition at a location.                       |
| `POST /appointment-definitions/{definitionId}/staff`     | Attach a staff member (`staff_id`, optional `is_active`). |

Both collections support `GET`, and each attachment supports `PUT` and `DELETE` at `.../locations/{id}` and `.../staff/{id}`.

Where staff come from depends on `use_staff_from_location`:

| `use_staff_from_location` | Staff source                                                                                   |
| ------------------------- | ---------------------------------------------------------------------------------------------- |
| `true`                    | Staff assigned to the requested location.                                                      |
| `false`                   | The explicit staff attached to the definition through `staff_ids` or the `.../staff` endpoint. |

<Note>When `use_staff_from_location` is `true`, you do not attach staff to the definition. Availability draws from the location's staff for the requested date. When it is `false`, attach the staff explicitly; their own hours still gate availability.</Note>

## Booking questionnaire

To capture answers at booking, attach a booking questionnaire. Send a `booking_questionnaire` object on the definition, either inline as a new questionnaire or by referencing an existing questionnaire's questions.

```shell theme={null}
curl -X POST "https://api.[tenant].getomneo.com/api/v3/appointment-definitions" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "handle": "bra-fitting",
    "name": "Bra fitting",
    "duration_minutes": 30,
    "booking_type": "approval_required",
    "booking_questionnaire": {
      "name": "Fitting intake",
      "handle": "fitting-intake",
      "is_active": true,
      "questions": [
        {
          "question_handle": "reason-for-visit",
          "mapping_key": "reason",
          "sort_order": 1,
          "is_required": true
        }
      ]
    }
  }'
```

Each question needs a `mapping_key`, which routes the answer to a destination. Reference a reusable Question by `question_id`, `question_handle`, or a specific `question_version_id`. Group questions into `pages` when the form needs sections. Omneo pins the questionnaire by version, so a booked appointment keeps the form version that was active when it was booked.

For how questions and questionnaires are built and how answers are routed, see [Building questionnaires](/dev-guides/feedback/building-questionnaires) and the [Questionnaires concept](/concepts/ratings/questionnaires).

## Notification targets

A definition can send a notification for each lifecycle event by pointing at a [Target](/concepts/automation/targets) and setting an offset. Each event has a target field and a matching pair of offset fields in days and hours.

| Event     | Target field          | Offset fields                                                   |
| --------- | --------------------- | --------------------------------------------------------------- |
| Created   | `created_target_id`   | `notify_created_offset_days`, `notify_created_offset_hours`     |
| Confirmed | `confirmed_target_id` | `notify_confirmed_offset_days`, `notify_confirmed_offset_hours` |
| Completed | `completed_target_id` | `notify_completed_offset_days`, `notify_completed_offset_hours` |
| Cancelled | `cancelled_target_id` | `notify_cancelled_offset_days`, `notify_cancelled_offset_hours` |
| Rejected  | `rejected_target_id`  | `notify_rejected_offset_days`, `notify_rejected_offset_hours`   |
| Reminder  | `reminder_target_id`  | `notify_reminder_offset_days`, `notify_reminder_offset_hours`   |

```shell theme={null}
curl -X PUT "https://api.[tenant].getomneo.com/api/v3/appointment-definitions/{definitionId}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "confirmed_target_id": 42,
    "reminder_target_id": 43,
    "notify_reminder_offset_days": 1,
    "notify_reminder_offset_hours": 0
  }'
```

The offset positions the notification relative to the appointment's scheduled start. A reminder offset of one day sends the reminder 24 hours before the booking. Rescheduling an appointment re-schedules its pending reminders.

## Visibility conditions

To control which definitions a Profile sees in a customer-facing booking flow, set a `visibility_condition`: a [JsonLogic](https://jsonlogic.com) rule evaluated against the requesting Profile's attributes.

```shell theme={null}
curl -X PUT "https://api.[tenant].getomneo.com/api/v3/appointment-definitions/{definitionId}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{ "visibility_condition": { "==": [1, 1] } }'
```

Read the definitions a Profile can see from the profile-scoped visibility endpoint:

```shell theme={null}
GET /api/v3/profiles/{profileId}/appointment-definitions/visibility
```

This returns only published, non-archived definitions whose `visibility_condition` evaluates to `true` for that Profile. A `null` `visibility_condition` never passes the check, so a definition with `null` is excluded from the visibility endpoint. To make a definition visible to every qualifying Profile, use a rule that always returns `true`, such as `{"==": [1, 1]}`. See [Visibility conditions](/concepts/visits/appointments#visibility-conditions) in the concept for how the rule is evaluated.

## Publishing a definition

Two flags control whether a definition is live.

| Flag           | Effect                                                                                                                                    |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `is_published` | When `true`, the definition is available to booking flows. Create definitions with `is_published: false` while configuring, then publish. |
| `is_archived`  | When `true`, the definition is retired and excluded from booking flows.                                                                   |

```shell theme={null}
curl -X PUT "https://api.[tenant].getomneo.com/api/v3/appointment-definitions/{definitionId}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{ "is_published": true }'
```

Customer-facing lists filter on both, so query `filter[is_published]=1&filter[is_archived]=0` to fetch only live definitions.

## The definition resource

A `GET /appointment-definitions/{definitionId}` returns the full definition wrapped in a `data` object: every configured field, a `has_booking_questionnaire` flag, and the loaded `normal_hours`, `special_hours`, `locations`, `staff`, and `booking_questionnaire` relations.

```json theme={null}
{
  "data": {
    "id": 1,
    "handle": "bra-fitting",
    "name": "Bra fitting",
    "description": null,
    "short_description": "A 30 minute fitting with a specialist.",
    "long_description": null,
    "icon": null,
    "image_url": null,
    "terms_conditions": null,
    "internal_notes": null,
    "visibility_condition": { "==": [1, 1] },
    "is_published": true,
    "is_archived": false,
    "duration_minutes": 30,
    "buffer_before_minutes": 5,
    "buffer_after_minutes": 5,
    "min_lead_minutes": 120,
    "max_advance_days": 30,
    "slot_interval_minutes": 15,
    "booking_type": "approval_required",
    "allow_customer_booking": true,
    "allow_walk_in": false,
    "requires_staff": true,
    "customer_must_select_staff": false,
    "use_staff_from_location": true,
    "max_concurrent_bookings": null,
    "allow_waitlist": true,
    "allow_queue": false,
    "meta": {},
    "created_target_id": null,
    "confirmed_target_id": 42,
    "cancelled_target_id": null,
    "rejected_target_id": null,
    "reminder_target_id": 43,
    "completed_target_id": null,
    "notify_created_offset_days": null,
    "notify_created_offset_hours": null,
    "notify_confirmed_offset_days": 0,
    "notify_confirmed_offset_hours": 0,
    "notify_cancelled_offset_days": null,
    "notify_cancelled_offset_hours": null,
    "notify_rejected_offset_days": null,
    "notify_rejected_offset_hours": null,
    "notify_reminder_offset_days": 1,
    "notify_reminder_offset_hours": 0,
    "notify_completed_offset_days": null,
    "notify_completed_offset_hours": null,
    "has_booking_questionnaire": true,
    "normal_hours": [
      { "id": 301, "day_of_week": "MON", "available_from": "09:00", "available_until": "17:00" },
      { "id": 302, "day_of_week": "TUE", "available_from": "09:00", "available_until": "17:00" }
    ],
    "special_hours": [
      {
        "id": 88,
        "name": "Public holiday",
        "is_repeating": false,
        "available_from": "10:00",
        "available_until": "14:00",
        "start_at": "2026-12-25",
        "end_at": "2026-12-25"
      }
    ],
    "locations": [
      { "id": 500, "location_id": 13, "is_active": true },
      { "id": 501, "location_id": 14, "is_active": true }
    ],
    "staff": [
      { "id": 700, "staff_id": "c3d582d4-b75f-47a9-d6fd-1d2b41d77e24", "is_active": true }
    ],
    "booking_questionnaire": {
      "name": "Fitting intake",
      "handle": "fitting-intake",
      "is_active": true,
      "questions": [
        {
          "question_handle": "reason-for-visit",
          "mapping_key": "reason",
          "sort_order": 1,
          "is_required": true
        }
      ]
    },
    "created_at": "2026-05-01T04:00:00Z",
    "updated_at": "2026-05-01T04:00:00Z"
  }
}
```

Every scalar field you set on create is echoed back: the copy fields, the scheduling fields, `booking_type`, the [capability flags](#booking-type-and-capability-flags), the [notification targets and offsets](#notification-targets), `visibility_condition`, `is_published`, `is_archived`, and `meta`, plus `created_at` and `updated_at` in UTC. The relations below are loaded on the read.

| Field                       | Type           | Notes                                                                                                              |
| --------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------ |
| `has_booking_questionnaire` | boolean        | Whether a booking questionnaire is attached. A quick check without inspecting `booking_questionnaire`.             |
| `normal_hours`              | array          | The weekly opening-hours entries, each `{ id, day_of_week, available_from, available_until }`.                     |
| `special_hours`             | array          | The dated overrides, each `{ id, name, is_repeating, available_from, available_until, start_at, end_at }`.         |
| `locations`                 | array          | The definition-location attachments, each `{ id, location_id, is_active }`.                                        |
| `staff`                     | array          | The attached staff, each `{ id, staff_id, is_active }`. Populated when the definition uses an explicit staff list. |
| `booking_questionnaire`     | object or null | The attached questionnaire with its `questions`, `null` when the definition has none.                              |

## End-to-end example: build and publish a definition

Build a definition in a draft state, configure it through the nested endpoints, then publish it once it is complete.

<Steps>
  <Step title="Create a draft">
    ```shell theme={null}
    curl -X POST "https://api.[tenant].getomneo.com/api/v3/appointment-definitions" \
      -H "Authorization: Bearer ${TOKEN}" \
      -H "Content-Type: application/json" \
      -d '{
        "handle": "bra-fitting",
        "name": "Bra fitting",
        "duration_minutes": 30,
        "booking_type": "approval_required",
        "requires_staff": true,
        "use_staff_from_location": true,
        "location_ids": [13, 14],
        "is_published": false
      }'
    ```

    The response is `201` with the generated `id`. Configure everything else while `is_published` is `false`.
  </Step>

  <Step title="Add opening hours">
    ```shell theme={null}
    curl -X POST "https://api.[tenant].getomneo.com/api/v3/appointment-definitions/1/normal-hours" \
      -H "Authorization: Bearer ${TOKEN}" \
      -H "Content-Type: application/json" \
      -d '{ "day_of_week": "MON", "available_from": "09:00", "available_until": "17:00" }'
    ```

    Repeat for each open day, and add `special-hours` for holidays.
  </Step>

  <Step title="Attach the booking questionnaire and notifications">
    ```shell theme={null}
    curl -X PUT "https://api.[tenant].getomneo.com/api/v3/appointment-definitions/1" \
      -H "Authorization: Bearer ${TOKEN}" \
      -H "Content-Type: application/json" \
      -d '{
        "confirmed_target_id": 42,
        "reminder_target_id": 43,
        "notify_reminder_offset_days": 1
      }'
    ```

    Attach the booking questionnaire inline or by reference as shown under [Booking questionnaire](#booking-questionnaire).
  </Step>

  <Step title="Publish">
    ```shell theme={null}
    curl -X PUT "https://api.[tenant].getomneo.com/api/v3/appointment-definitions/1" \
      -H "Authorization: Bearer ${TOKEN}" \
      -H "Content-Type: application/json" \
      -d '{ "is_published": true }'
    ```

    Read the definition back to confirm its `normal_hours`, `locations`, `staff`, and `booking_questionnaire` are all in place before customers can book.
  </Step>
</Steps>

## Errors

| Status | When                                                                                                                                 |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| `401`  | Missing or invalid bearer token.                                                                                                     |
| `403`  | The token lacks the required scope, or the actor lacks permission.                                                                   |
| `404`  | The definition does not exist, or a nested hours, location, or staff entry does not belong to the definition in the URL.             |
| `422`  | Validation failed. Create requires `handle`, `name`, `duration_minutes`, and `booking_type`; a missing or invalid one returns `422`. |

## Edge cases and gotchas

* **`max_concurrent_bookings: null` means unlimited.** Leave it `null` for group services with no per-slot cap. Set an integer only when a slot has a real limit. It has no effect on staff-required definitions, where capacity is one per staff member per slot.
* **`customer_must_select_staff` needs `requires_staff`.** The customer can only be forced to pick a staff member on a definition that assigns staff, so set `requires_staff: true` as well.
* **Opening hours are plain local times.** `available_from` and `available_until` are local to the location's timezone and carry no timezone themselves, unlike appointment datetimes, which are stored in UTC.
* **Special hours win.** When a date matches both a normal-hours day and a special-hours entry, the special hour takes precedence for that date.
* **Booking questionnaires are pinned by version.** A booked appointment keeps the questionnaire version that was active at booking, so editing the form later never rewrites historical answers.
* **A `null` `visibility_condition` is excluded.** Such a definition never passes the profile-scoped visibility check. Use `{"==": [1, 1]}` to make it visible to every qualifying Profile.

## Related

* [Appointments concept](/concepts/visits/appointments)
* [Working with appointments](/dev-guides/visits/appointments)
* [Walk-in queues and waitlists](/dev-guides/visits/walk-in-queues-and-waitlists)
* [Building questionnaires](/dev-guides/feedback/building-questionnaires)
* [Targets](/concepts/automation/targets)
* [Add appointment definition API](/api-reference/appointment-definition/add-appointment-definition)
* [Browse appointment definitions API](/api-reference/appointment-definition/browse-appointment-definitions)
* [Add appointment definition normal hour API](/api-reference/appointment-definition-normal-hour/add-appointment-definition-normal-hour)
* [Add appointment definition staff API](/api-reference/appointment-definition-staff/add-appointment-definition-staff)
* [Browse available appointment slots API](/api-reference/appointment-availability/browse-available-appointment-slots)
* [Browse available appointment staff API](/api-reference/appointment-availability/browse-available-appointment-staff)
* [Browse visible appointment definitions API](/api-reference/profile-appointment/browse-visible-appointment-definitions)
