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

# Calendar events and bookings

> Read calendar events and manage bookings through the FITsociety Public API.

Calendar event writes are not exposed in Public API v1 because they affect
recurrence, calendar projections, notifications, external calendar sync, and
booking financial handling. Public API write access is limited to guarded
booking, recurring-booking, calendar-task, booking-request, availability
template, and availability closure actions.

## Scopes

| Scope                         | Allows                                                                        |
| :---------------------------- | :---------------------------------------------------------------------------- |
| `calendar_events:read`        | List and read calendar event DTOs.                                            |
| `event_types:read`            | List and read event type labels used by event templates.                      |
| `event_types:write`           | Create, update, and archive event type labels.                                |
| `event_templates:read`        | List and read event templates used by calendar availability and bookings.     |
| `event_templates:write`       | Create, update, and archive event templates.                                  |
| `recurring_bookings:read`     | List and read recurring booking chains.                                       |
| `recurring_bookings:write`    | Update safe recurring booking settings and cancel a recurring chain.          |
| `calendar_tasks:read`         | List and read blocking calendar tasks.                                        |
| `calendar_tasks:write`        | Create, update, and cancel blocking calendar tasks.                           |
| `booking_requests:read`       | List booking requests.                                                        |
| `booking_requests:write`      | Approve or reject pending booking requests.                                   |
| `coach_availability:read`     | Read coach availability templates.                                            |
| `coach_availability:write`    | Replace coach availability templates.                                         |
| `location_availability:read`  | Read location availability templates.                                         |
| `location_availability:write` | Replace location availability templates.                                      |
| `availability_rules:read`     | Read availability closure metadata.                                           |
| `availability_rules:write`    | Replace company availability closure metadata.                                |
| `availability:read`           | List bookable availability slots and receive short-lived availability tokens. |
| `bookings:read`               | List bookings, read booking details, and read cancellation context.           |
| `bookings:write`              | Create bookings, update attendance, and cancel bookings.                      |

## Event types

Event types are calendar labels/categories used by event templates for filtering
and display. Public API consumers can manage these directly when an integration
needs to keep appointment categories in sync.

```http theme={null}
GET /public/v1/event-types
POST /public/v1/event-types
GET /public/v1/event-types/{eventTypeId}
PATCH /public/v1/event-types/{eventTypeId}
POST /public/v1/event-types/{eventTypeId}/archive
```

Required scopes: `event_types:read` for reads, `event_types:write` for writes.

Event type DTO fields:

| Field            | Type    | Nullable | Description                               |
| :--------------- | :------ | :------- | :---------------------------------------- |
| `eventTypeId`    | string  | no       | Event type ID.                            |
| `name.en`        | string  | yes      | English name.                             |
| `name.nl`        | string  | yes      | Dutch name.                               |
| `sourceLanguage` | string  | no       | `en` or `nl`.                             |
| `normalizedName` | string  | no       | Lowercase display name used for matching. |
| `archived`       | boolean | no       | Whether the event type is archived.       |
| `createdAt`      | string  | yes      | Creation timestamp.                       |
| `updatedAt`      | string  | yes      | Update timestamp.                         |

Validation:

| Input            | Rule                                                                  |
| :--------------- | :-------------------------------------------------------------------- |
| `eventTypeId`    | Must be a valid MongoDB ObjectId.                                     |
| `name`           | Required for create and update. Accepts a string or localized object. |
| `sourceLanguage` | Normalized to `en` or `nl`; unsupported values fall back to `en`.     |

## Event templates

Event templates are the public calendar template resource. They define what can
be scheduled or booked: name, duration, default location, capacity, booking
rules, visibility, pricing, credits, and the optional `eventType` label.

```http theme={null}
GET /public/v1/event-templates?page=1&limit=100&eventType=Group
POST /public/v1/event-templates
GET /public/v1/event-templates/{eventTemplateId}
PATCH /public/v1/event-templates/{eventTemplateId}
POST /public/v1/event-templates/{eventTemplateId}/archive
```

Required scopes: `event_templates:read` for reads, `event_templates:write` for
writes.

List response fields:

| Field                   | Type    | Nullable | Description                           |
| :---------------------- | :------ | :------- | :------------------------------------ |
| `data.page`             | integer | no       | Current page.                         |
| `data.limit`            | integer | no       | Page size after applying the maximum. |
| `data.total`            | integer | no       | Total matching event templates.       |
| `data.totalPages`       | integer | no       | Number of pages.                      |
| `data.hasNextPage`      | boolean | no       | Whether another page exists.          |
| `data.hasPrevPage`      | boolean | no       | Whether a previous page exists.       |
| `data.eventTemplates[]` | array   | no       | Event template DTOs.                  |

Event template DTO fields:

| Field                          | Type    | Nullable | Description                                                             |
| :----------------------------- | :------ | :------- | :---------------------------------------------------------------------- |
| `eventTemplateId`              | string  | no       | Event template ID.                                                      |
| `name.en`                      | string  | yes      | English name.                                                           |
| `name.nl`                      | string  | yes      | Dutch name.                                                             |
| `sourceLanguage`               | string  | no       | `en` or `nl`.                                                           |
| `description.en`               | string  | yes      | English description.                                                    |
| `description.nl`               | string  | yes      | Dutch description.                                                      |
| `duration`                     | string  | no       | Duration value as stored by the calendar module.                        |
| `durationType`                 | string  | no       | Duration unit, usually `Minutes`.                                       |
| `defaultLocationId`            | string  | no       | Default location ID.                                                    |
| `color`                        | string  | no       | Display color.                                                          |
| `groupSize`                    | number  | yes      | Maximum group size when configured.                                     |
| `enforceGroupSizeLimit`        | boolean | no       | Whether group size is enforced.                                         |
| `isOnline`                     | boolean | no       | Whether the template is online.                                         |
| `blockCalendarCheck`           | boolean | no       | Whether normal calendar availability checks are bypassed internally.    |
| `bookingOption`                | string  | no       | Booking option key.                                                     |
| `cancellationPolicy`           | string  | no       | Cancellation policy key.                                                |
| `waitingList`                  | string  | no       | Waiting-list mode key.                                                  |
| `eventType`                    | string  | no       | Calendar label/category for filtering and display.                      |
| `clientCalendarVisibilityMode` | string  | no       | Client calendar visibility mode.                                        |
| `pricing.price`                | number  | yes      | Stored price.                                                           |
| `pricing.currency`             | string  | no       | ISO currency code.                                                      |
| `pricing.vatPercentage`        | number  | yes      | VAT percentage.                                                         |
| `creditRequirement.credits`    | number  | no       | Credits required per booking.                                           |
| `mediaCount`                   | integer | no       | Count of linked media records without exposing URLs or storage objects. |
| `archived`                     | boolean | no       | Whether the template is archived.                                       |
| `createdAt`                    | string  | yes      | Creation timestamp.                                                     |
| `updatedAt`                    | string  | yes      | Update timestamp.                                                       |

Validation:

| Input                                                                     | Rule                                                              |
| :------------------------------------------------------------------------ | :---------------------------------------------------------------- |
| `page`                                                                    | Integer, minimum `1`.                                             |
| `limit`                                                                   | Integer, default `100`, maximum `100`.                            |
| `eventType`                                                               | Optional exact label filter.                                      |
| `eventTemplateId`                                                         | Must be a valid MongoDB ObjectId.                                 |
| `name`                                                                    | Required on create. Accepts a string or localized object.         |
| `description`                                                             | Accepts a string or localized object.                             |
| `sourceLanguage`                                                          | Normalized to `en` or `nl`; unsupported values fall back to `en`. |
| `duration`                                                                | Required on create.                                               |
| `defaultLocationId`                                                       | Required on create and must reference an active company location. |
| `groupSize`, `creditPerBooking`, `pricing.price`, `pricing.vatPercentage` | Must be numeric when supplied.                                    |
| `pricing.currency`                                                        | Uppercased; defaults to `EUR` when omitted inside `pricing`.      |

Omitted: raw media arrays, image/video storage URLs, change history, coach-only
notes, product details, provider metadata, and internal recurrence/event
documents.

## List calendar events

```http theme={null}
GET /public/v1/calendar-events?dateFrom=2026-07-01T00:00:00.000Z&dateTo=2026-07-31T23:59:59.999Z
Authorization: Bearer <access_token>
```

Required scope: `calendar_events:read`

Validation:

| Parameter  | Type          | Required | Rule                                           |
| :--------- | :------------ | :------- | :--------------------------------------------- |
| `dateFrom` | ISO date-time | yes      | Valid date-time.                               |
| `dateTo`   | ISO date-time | yes      | Valid date-time, must be after `dateFrom`.     |
| `coachId`  | string        | no       | Must be a valid coach ObjectId in the company. |

Range limit: maximum 92 days.

Response fields:

| Field                       | Type    | Nullable | Description                                              |
| :-------------------------- | :------ | :------- | :------------------------------------------------------- |
| `data.dateFrom`             | string  | no       | UTC ISO range start after parsing.                       |
| `data.dateTo`               | string  | no       | UTC ISO range end after parsing.                         |
| `data.truncated`            | boolean | no       | `true` when recurrence or result safety limits were hit. |
| `data.events[]`             | array   | no       | Calendar occurrence summaries.                           |
| `data.events[].eventId`     | string  | no       | Calendar event document ID.                              |
| `data.events[].name`        | string  | no       | Localized event name.                                    |
| `data.events[].startAt`     | string  | yes      | Occurrence start.                                        |
| `data.events[].endAt`       | string  | yes      | Occurrence end.                                          |
| `data.events[].isRecurring` | boolean | no       | Whether the occurrence comes from recurrence.            |

## Get calendar event

```http theme={null}
GET /public/v1/calendar-events/{eventId}
Authorization: Bearer <access_token>
```

Required scope: `calendar_events:read`

Response fields:

| Field                          | Type    | Nullable | Description                                        |
| :----------------------------- | :------ | :------- | :------------------------------------------------- |
| `data.event.eventId`           | string  | no       | Calendar event ID.                                 |
| `data.event.masterId`          | string  | yes      | Master recurring event ID for exception instances. |
| `data.event.name`              | string  | no       | Localized event name.                              |
| `data.event.startAt`           | string  | yes      | Event start.                                       |
| `data.event.endAt`             | string  | yes      | Event end.                                         |
| `data.event.isRecurring`       | boolean | no       | Whether this is recurring or linked to recurrence. |
| `data.event.timeZone`          | string  | no       | Event timezone when configured.                    |
| `data.event.templateId`        | string  | yes      | Event template ID.                                 |
| `data.event.locationId`        | string  | yes      | Location ID.                                       |
| `data.event.coaches[]`         | array   | no       | Assigned coaches.                                  |
| `data.event.coaches[].coachId` | string  | no       | Coach ID.                                          |
| `data.event.coaches[].name`    | string  | no       | Coach display name.                                |

## Recurring bookings

Recurring booking endpoints expose operational metadata and guarded chain-level
updates. They do not expose raw recurrence internals, external sync state,
notification payloads, billing snapshots, or audit history.

### List recurring bookings

```http theme={null}
GET /public/v1/recurring-bookings?page=1&limit=100
Authorization: Bearer <access_token>
```

Required scope: `recurring_bookings:read`

Response fields:

| Field                                         | Type    | Nullable | Description                         |
| :-------------------------------------------- | :------ | :------- | :---------------------------------- |
| `data.page`                                   | integer | no       | Current page.                       |
| `data.limit`                                  | integer | no       | Page size after cap, maximum `100`. |
| `data.total`                                  | integer | no       | Total recurring booking chains.     |
| `data.totalPages`                             | integer | no       | Total pages.                        |
| `data.hasNextPage`                            | boolean | no       | Next page availability.             |
| `data.hasPrevPage`                            | boolean | no       | Previous page availability.         |
| `data.recurringBookings[].recurringBookingId` | string  | no       | Recurring booking chain ID.         |
| `data.recurringBookings[].client.name`        | string  | no       | Client display name.                |
| `data.recurringBookings[].status`             | string  | no       | Chain status.                       |
| `data.recurringBookings[].repeatInterval`     | string  | no       | Repeat interval.                    |
| `data.recurringBookings[].startDate`          | string  | yes      | Chain start.                        |
| `data.recurringBookings[].endDate`            | string  | yes      | Chain end.                          |
| `data.recurringBookings[].numberOfSessions`   | number  | yes      | Session count when configured.      |
| `data.recurringBookings[].isContinuous`       | boolean | no       | Whether chain is continuous.        |

Detail response fields:

| Field                                            | Type    | Nullable | Description                    |
| :----------------------------------------------- | :------ | :------- | :----------------------------- |
| `data.recurringBooking.recurringBookingId`       | string  | no       | Recurring booking chain ID.    |
| `data.recurringBooking.status`                   | string  | no       | Chain status.                  |
| `data.recurringBooking.repeatInterval`           | string  | no       | Repeat interval.               |
| `data.recurringBooking.startDate`                | string  | yes      | Chain start.                   |
| `data.recurringBooking.endDate`                  | string  | yes      | Chain end.                     |
| `data.recurringBooking.numberOfSessions`         | number  | yes      | Session count when configured. |
| `data.recurringBooking.isContinuous`             | boolean | no       | Whether chain is continuous.   |
| `data.recurringBooking.paymentMethod`            | string  | no       | Chain payment method.          |
| `data.recurringBooking.eventId`                  | string  | yes      | Linked calendar event ID.      |
| `data.recurringBooking.client.clientId`          | string  | no       | Client ID.                     |
| `data.recurringBooking.client.name`              | string  | no       | Client display name.           |
| `data.recurringBooking.pauseWindows[].startDate` | string  | yes      | Active pause window start.     |
| `data.recurringBooking.pauseWindows[].endDate`   | string  | yes      | Active pause window end.       |
| `data.recurringBooking.pauseWindows[].reason`    | string  | no       | Public pause reason.           |

### Update recurring booking

```http theme={null}
PATCH /public/v1/recurring-bookings/{recurringBookingId}
Authorization: Bearer <access_token>
Content-Type: application/json
```

Required scope: `recurring_bookings:write`

Request body:

| Field                   | Type          | Required    | Rule                                                                  |
| :---------------------- | :------------ | :---------- | :-------------------------------------------------------------------- |
| `paymentMethod`         | string        | no          | `Free`, `CreditsOnly`, or `CreditsOrInvoice`.                         |
| `endDate`               | ISO date-time | conditional | One end mode only. Must be after the chain start date.                |
| `numberOfSessions`      | integer       | conditional | One end mode only. Minimum `1`, maximum `520`.                        |
| `isContinuous`          | boolean       | conditional | One end mode only. Clears `endDate` and `numberOfSessions` when true. |
| `pauseWindow.startDate` | ISO date-time | no          | Required when `pauseWindow` is supplied.                              |
| `pauseWindow.endDate`   | ISO date-time | no          | Required when `pauseWindow` is supplied and must be after start.      |
| `pauseWindow.reason`    | string        | no          | Stored reason, capped at 500 characters.                              |
| `clearPauseWindows`     | boolean       | no          | When true, active pause windows are marked cleared.                   |

Unknown fields are rejected with `PUBLIC_API_UNKNOWN_FIELDS`.

Response fields:

| Field                                            | Type      | Nullable | Description                                 |
| :----------------------------------------------- | :-------- | :------- | :------------------------------------------ |
| `data.recurringBooking.recurringBookingId`       | string    | no       | Recurring booking chain ID.                 |
| `data.recurringBooking.status`                   | string    | no       | Chain status.                               |
| `data.recurringBooking.repeatInterval`           | string    | no       | Repeat interval.                            |
| `data.recurringBooking.startDate`                | string    | yes      | Chain start.                                |
| `data.recurringBooking.endDate`                  | string    | yes      | Chain end.                                  |
| `data.recurringBooking.numberOfSessions`         | number    | yes      | Session count when configured.              |
| `data.recurringBooking.isContinuous`             | boolean   | no       | Whether chain is continuous.                |
| `data.recurringBooking.paymentMethod`            | string    | no       | Chain payment method.                       |
| `data.recurringBooking.eventId`                  | string    | yes      | Linked calendar event ID.                   |
| `data.recurringBooking.client.clientId`          | string    | no       | Client ID.                                  |
| `data.recurringBooking.client.name`              | string    | no       | Client display name.                        |
| `data.recurringBooking.pauseWindows[].startDate` | string    | yes      | Active pause window start.                  |
| `data.recurringBooking.pauseWindows[].endDate`   | string    | yes      | Active pause window end.                    |
| `data.recurringBooking.pauseWindows[].reason`    | string    | no       | Public pause reason.                        |
| `data.updatedFields[]`                           | string\[] | no       | Internal field groups changed by the patch. |

### Cancel recurring booking

```http theme={null}
POST /public/v1/recurring-bookings/{recurringBookingId}/cancel
Authorization: Bearer <access_token>
Content-Type: application/json
```

Required scope: `recurring_bookings:write`

Request body:

| Field    | Type   | Required | Rule                                           |
| :------- | :----- | :------- | :--------------------------------------------- |
| `reason` | string | no       | Stored audit reason, capped at 500 characters. |

The endpoint marks the recurring chain as `Cancelled`. It does not hard delete
the chain and does not retroactively delete existing booking records.

Response fields:

| Field                                            | Type    | Nullable | Description                          |
| :----------------------------------------------- | :------ | :------- | :----------------------------------- |
| `data.recurringBooking.recurringBookingId`       | string  | no       | Recurring booking chain ID.          |
| `data.recurringBooking.status`                   | string  | no       | `Cancelled` after success.           |
| `data.recurringBooking.repeatInterval`           | string  | no       | Repeat interval.                     |
| `data.recurringBooking.startDate`                | string  | yes      | Chain start.                         |
| `data.recurringBooking.endDate`                  | string  | yes      | Chain end.                           |
| `data.recurringBooking.numberOfSessions`         | number  | yes      | Session count when configured.       |
| `data.recurringBooking.isContinuous`             | boolean | no       | Whether chain is continuous.         |
| `data.recurringBooking.paymentMethod`            | string  | no       | Chain payment method.                |
| `data.recurringBooking.eventId`                  | string  | yes      | Linked calendar event ID.            |
| `data.recurringBooking.client.clientId`          | string  | no       | Client ID.                           |
| `data.recurringBooking.client.name`              | string  | no       | Client display name.                 |
| `data.recurringBooking.pauseWindows[].startDate` | string  | yes      | Active pause window start.           |
| `data.recurringBooking.pauseWindows[].endDate`   | string  | yes      | Active pause window end.             |
| `data.recurringBooking.pauseWindows[].reason`    | string  | no       | Public pause reason.                 |
| `data.cancelled`                                 | boolean | no       | `true` when the chain was cancelled. |

## Calendar tasks

```http theme={null}
GET /public/v1/calendar-tasks?dateFrom=2026-07-01T00:00:00.000Z&dateTo=2026-07-31T23:59:59.999Z
Authorization: Bearer <access_token>
```

Required scope: `calendar_tasks:read`

Response fields:

| Field                            | Type    | Nullable | Description                                           |
| :------------------------------- | :------ | :------- | :---------------------------------------------------- |
| `data.dateFrom`                  | string  | no       | UTC ISO range start after parsing.                    |
| `data.dateTo`                    | string  | no       | UTC ISO range end after parsing.                      |
| `data.truncated`                 | boolean | no       | `true` when recurrence/result safety limits were hit. |
| `data.tasks[].taskId`            | string  | no       | Task ID.                                              |
| `data.tasks[].title`             | string  | no       | Task title.                                           |
| `data.tasks[].notes`             | string  | no       | Task notes.                                           |
| `data.tasks[].blockingScope`     | string  | no       | Blocking scope.                                       |
| `data.tasks[].startAt`           | string  | yes      | Start timestamp.                                      |
| `data.tasks[].endAt`             | string  | yes      | End timestamp.                                        |
| `data.tasks[].isRecurring`       | boolean | no       | Whether recurring.                                    |
| `data.tasks[].timeZone`          | string  | no       | Timezone.                                             |
| `data.tasks[].color`             | string  | no       | Display color.                                        |
| `data.tasks[].coaches[]`         | array   | no       | Assigned coach summaries.                             |
| `data.tasks[].coaches[].coachId` | string  | no       | Coach ID.                                             |
| `data.tasks[].coaches[].name`    | string  | no       | Coach display name.                                   |

`GET /public/v1/calendar-tasks/{taskId}` returns these fields under
`data.task.*`:

| Field                         | Type    | Nullable | Description         |
| :---------------------------- | :------ | :------- | :------------------ |
| `data.task.taskId`            | string  | no       | Task ID.            |
| `data.task.title`             | string  | no       | Task title.         |
| `data.task.notes`             | string  | no       | Task notes.         |
| `data.task.blockingScope`     | string  | no       | Blocking scope.     |
| `data.task.startAt`           | string  | yes      | Start timestamp.    |
| `data.task.endAt`             | string  | yes      | End timestamp.      |
| `data.task.isRecurring`       | boolean | no       | Whether recurring.  |
| `data.task.timeZone`          | string  | no       | Timezone.           |
| `data.task.color`             | string  | no       | Display color.      |
| `data.task.coaches[].coachId` | string  | no       | Coach ID.           |
| `data.task.coaches[].name`    | string  | no       | Coach display name. |

### Create calendar task

```http theme={null}
POST /public/v1/calendar-tasks
Authorization: Bearer <access_token>
Content-Type: application/json
```

Required scope: `calendar_tasks:write`

Request body:

| Field                | Type          | Required    | Rule                                              |
| :------------------- | :------------ | :---------- | :------------------------------------------------ |
| `title`              | string        | yes         | Task title accepted by the calendar task helper.  |
| `startAt`            | ISO date-time | yes         | Start timestamp.                                  |
| `endAt`              | ISO date-time | conditional | End timestamp when not using duration.            |
| `durationMinutes`    | integer       | conditional | Duration when not using explicit `endAt`.         |
| `notes`              | string        | no          | Task notes.                                       |
| `assignedCoachIds[]` | string\[]     | no          | Coaches must belong to the authenticated company. |

Unknown fields are rejected with `PUBLIC_API_UNKNOWN_FIELDS`.

Response fields:

| Field                         | Type    | Nullable | Description         |
| :---------------------------- | :------ | :------- | :------------------ |
| `data.task.taskId`            | string  | no       | Task ID.            |
| `data.task.title`             | string  | no       | Task title.         |
| `data.task.notes`             | string  | no       | Task notes.         |
| `data.task.blockingScope`     | string  | no       | Blocking scope.     |
| `data.task.startAt`           | string  | yes      | Start timestamp.    |
| `data.task.endAt`             | string  | yes      | End timestamp.      |
| `data.task.isRecurring`       | boolean | no       | Whether recurring.  |
| `data.task.timeZone`          | string  | no       | Timezone.           |
| `data.task.color`             | string  | no       | Display color.      |
| `data.task.coaches[].coachId` | string  | no       | Coach ID.           |
| `data.task.coaches[].name`    | string  | no       | Coach display name. |

### Update calendar task

```http theme={null}
PATCH /public/v1/calendar-tasks/{taskId}
Authorization: Bearer <access_token>
Content-Type: application/json
```

Required scope: `calendar_tasks:write`

Allowed fields: `title`, `startAt`, `endAt`, `durationMinutes`, `notes`, and
`assignedCoachIds`. Unknown fields are rejected. Empty updates return
`NOTHING_TO_UPDATE`.

Response fields:

| Field                         | Type    | Nullable | Description         |
| :---------------------------- | :------ | :------- | :------------------ |
| `data.task.taskId`            | string  | no       | Task ID.            |
| `data.task.title`             | string  | no       | Task title.         |
| `data.task.notes`             | string  | no       | Task notes.         |
| `data.task.blockingScope`     | string  | no       | Blocking scope.     |
| `data.task.startAt`           | string  | yes      | Start timestamp.    |
| `data.task.endAt`             | string  | yes      | End timestamp.      |
| `data.task.isRecurring`       | boolean | no       | Whether recurring.  |
| `data.task.timeZone`          | string  | no       | Timezone.           |
| `data.task.color`             | string  | no       | Display color.      |
| `data.task.coaches[].coachId` | string  | no       | Coach ID.           |
| `data.task.coaches[].name`    | string  | no       | Coach display name. |

### Cancel calendar task

```http theme={null}
POST /public/v1/calendar-tasks/{taskId}/cancel
Authorization: Bearer <access_token>
```

Required scope: `calendar_tasks:write`

This uses the existing calendar task cancellation helper. External callers
should treat it as a cancel operation, not as a generic hard-delete API.

Response fields:

| Field                                 | Type    | Nullable | Description                                            |
| :------------------------------------ | :------ | :------- | :----------------------------------------------------- |
| `data.taskId`                         | string  | no       | Cancelled task ID.                                     |
| `data.previousState.recurrenceStatus` | string  | no       | Previous recurrence status, usually `confirmed`.       |
| `data.newState.recurrenceStatus`      | string  | no       | `cancelled`.                                           |
| `data.newState.deleted`               | boolean | no       | `true` when the task record was removed by the helper. |

## Booking requests

```http theme={null}
GET /public/v1/booking-requests?status=pending
Authorization: Bearer <access_token>
```

Required scope: `booking_requests:read`

Response fields:

| Field                                            | Type    | Nullable | Description              |
| :----------------------------------------------- | :------ | :------- | :----------------------- |
| `data.bookingRequests[].bookingRequestId`        | string  | no       | Booking request ID.      |
| `data.bookingRequests[].client.clientId`         | string  | no       | Client ID.               |
| `data.bookingRequests[].client.name`             | string  | no       | Client display name.     |
| `data.bookingRequests[].calendarEventId`         | string  | yes      | Calendar event ID.       |
| `data.bookingRequests[].lessonId`                | string  | yes      | Lesson/template ID.      |
| `data.bookingRequests[].bookingId`               | string  | yes      | Linked booking ID.       |
| `data.bookingRequests[].instanceDate`            | string  | yes      | Recurring instance date. |
| `data.bookingRequests[].status`                  | string  | no       | Request status.          |
| `data.bookingRequests[].capacity.booked`         | number  | yes      | Booked count.            |
| `data.bookingRequests[].capacity.waiting`        | number  | yes      | Waiting count.           |
| `data.bookingRequests[].capacity.total`          | number  | yes      | Total capacity.          |
| `data.bookingRequests[].capacity.available`      | number  | yes      | Available seats.         |
| `data.bookingRequests[].capacity.hasWaitingList` | boolean | no       | Waiting-list flag.       |
| `data.bookingRequests[].lesson.name`             | string  | no       | Lesson name.             |
| `data.bookingRequests[].lesson.type`             | string  | no       | Lesson type.             |
| `data.bookingRequests[].createdAt`               | string  | yes      | Creation timestamp.      |
| `data.bookingRequests[].updatedAt`               | string  | yes      | Last update timestamp.   |

Not exposed: client email, client image, location address, lesson descriptions,
credit/payment internals, accept/reject mutation internals, or notification
payloads.

### Approve booking request

```http theme={null}
POST /public/v1/booking-requests/{bookingRequestId}/approve
Authorization: Bearer <access_token>
Content-Type: application/json
```

Required scope: `booking_requests:write`

Request body:

| Field            | Type    | Required | Rule                                                              |
| :--------------- | :------ | :------- | :---------------------------------------------------------------- |
| `notifyClient`   | boolean | no       | Whether the client should be notified.                            |
| `clientMessage`  | string  | no       | Optional message accepted by the booking request helper.          |
| `capacityAction` | string  | no       | Empty string, `reserve`, `waitlist_if_full`, or `force_overbook`. |

The request must be pending, company-owned, and linked to a client visible to the
authenticated Public API client.

Response fields:

| Field                                              | Type    | Nullable | Description                  |
| :------------------------------------------------- | :------ | :------- | :--------------------------- |
| `data.bookingRequest.bookingRequestId`             | string  | no       | Booking request ID.          |
| `data.bookingRequest.client.clientId`              | string  | no       | Client ID.                   |
| `data.bookingRequest.client.name`                  | string  | no       | Client display name.         |
| `data.bookingRequest.calendarEventId`              | string  | yes      | Calendar event ID.           |
| `data.bookingRequest.lessonId`                     | string  | yes      | Lesson/template ID.          |
| `data.bookingRequest.bookingId`                    | string  | yes      | Linked booking ID.           |
| `data.bookingRequest.instanceDate`                 | string  | yes      | Recurring instance date.     |
| `data.bookingRequest.status`                       | string  | no       | Updated request status.      |
| `data.bookingRequest.capacity.booked`              | number  | yes      | Booked count.                |
| `data.bookingRequest.capacity.waiting`             | number  | yes      | Waiting count.               |
| `data.bookingRequest.capacity.total`               | number  | yes      | Total capacity.              |
| `data.bookingRequest.capacity.available`           | number  | yes      | Available seats.             |
| `data.bookingRequest.capacity.hasWaitingList`      | boolean | no       | Waiting-list flag.           |
| `data.bookingRequest.capacity.waitingListCapacity` | number  | yes      | Waiting-list capacity.       |
| `data.bookingRequest.lesson.name`                  | string  | no       | Lesson name.                 |
| `data.bookingRequest.lesson.type`                  | string  | no       | Lesson type.                 |
| `data.bookingRequest.lesson.enforceGroupSizeLimit` | boolean | no       | Group-size enforcement flag. |
| `data.bookingRequest.createdAt`                    | string  | yes      | Creation timestamp.          |
| `data.bookingRequest.updatedAt`                    | string  | yes      | Last update timestamp.       |
| `data.action.status`                               | string  | no       | `accepted`.                  |
| `data.action.appliedAt`                            | string  | no       | Decision timestamp.          |

### Reject booking request

```http theme={null}
POST /public/v1/booking-requests/{bookingRequestId}/reject
Authorization: Bearer <access_token>
Content-Type: application/json
```

Required scope: `booking_requests:write`

Request body:

| Field                      | Type    | Required | Rule                                                     |
| :------------------------- | :------ | :------- | :------------------------------------------------------- |
| `notifyClient`             | boolean | no       | Whether the client should be notified.                   |
| `clientMessage`            | string  | no       | Optional message accepted by the booking request helper. |
| `rejectionReason`          | string  | no       | Rejection reason.                                        |
| `canClientRequestAgain`    | boolean | no       | Whether the client may submit another request.           |
| `requestAgainAfterMinutes` | integer | no       | Delay before another request is allowed.                 |

Response fields:

| Field                                              | Type    | Nullable | Description                  |
| :------------------------------------------------- | :------ | :------- | :--------------------------- |
| `data.bookingRequest.bookingRequestId`             | string  | no       | Booking request ID.          |
| `data.bookingRequest.client.clientId`              | string  | no       | Client ID.                   |
| `data.bookingRequest.client.name`                  | string  | no       | Client display name.         |
| `data.bookingRequest.calendarEventId`              | string  | yes      | Calendar event ID.           |
| `data.bookingRequest.lessonId`                     | string  | yes      | Lesson/template ID.          |
| `data.bookingRequest.bookingId`                    | string  | yes      | Linked booking ID.           |
| `data.bookingRequest.instanceDate`                 | string  | yes      | Recurring instance date.     |
| `data.bookingRequest.status`                       | string  | no       | Updated request status.      |
| `data.bookingRequest.capacity.booked`              | number  | yes      | Booked count.                |
| `data.bookingRequest.capacity.waiting`             | number  | yes      | Waiting count.               |
| `data.bookingRequest.capacity.total`               | number  | yes      | Total capacity.              |
| `data.bookingRequest.capacity.available`           | number  | yes      | Available seats.             |
| `data.bookingRequest.capacity.hasWaitingList`      | boolean | no       | Waiting-list flag.           |
| `data.bookingRequest.capacity.waitingListCapacity` | number  | yes      | Waiting-list capacity.       |
| `data.bookingRequest.lesson.name`                  | string  | no       | Lesson name.                 |
| `data.bookingRequest.lesson.type`                  | string  | no       | Lesson type.                 |
| `data.bookingRequest.lesson.enforceGroupSizeLimit` | boolean | no       | Group-size enforcement flag. |
| `data.bookingRequest.createdAt`                    | string  | yes      | Creation timestamp.          |
| `data.bookingRequest.updatedAt`                    | string  | yes      | Last update timestamp.       |
| `data.action.status`                               | string  | no       | `rejected`.                  |
| `data.action.appliedAt`                            | string  | no       | Decision timestamp.          |

## Coach availability

Coach availability endpoints manage the working-time template for one coach.
Use these endpoints for the same kind of availability management shown in the
coach calendar settings UI. They do not return client-bookable slots.

```http theme={null}
GET /public/v1/coaches/{coachId}/availability
Authorization: Bearer <access_token>
```

Required scope: `coach_availability:read`

Response fields:

| Field                                                                                            | Type      | Nullable | Description                                                           |
| :----------------------------------------------------------------------------------------------- | :-------- | :------- | :-------------------------------------------------------------------- |
| `data.coachAvailability`                                                                         | object    | yes      | Coach availability template, or `null` when none has been configured. |
| `data.coachAvailability.coachId`                                                                 | string    | no       | Coach ID from the path.                                               |
| `data.coachAvailability.availabilityPeriods[]`                                                   | array     | no       | Date-ranged weekly availability templates.                            |
| `data.coachAvailability.availabilityPeriods[].startDate`                                         | string    | yes      | Period start in UTC.                                                  |
| `data.coachAvailability.availabilityPeriods[].endDate`                                           | string    | yes      | Period end in UTC, or `null` for open-ended periods.                  |
| `data.coachAvailability.availabilityPeriods[].weeklyPattern[]`                                   | array     | no       | Weekly day rules for this period.                                     |
| `data.coachAvailability.availabilityPeriods[].weeklyPattern[].dayOfWeek`                         | integer   | yes      | Day index, `0` for Sunday through `6` for Saturday.                   |
| `data.coachAvailability.availabilityPeriods[].weeklyPattern[].isDayOff`                          | boolean   | no       | Whether this weekday is blocked.                                      |
| `data.coachAvailability.availabilityPeriods[].weeklyPattern[].slots[]`                           | array     | no       | Coach working-time slots for the day.                                 |
| `data.coachAvailability.availabilityPeriods[].weeklyPattern[].slots[].locationId`                | string    | no       | Location ID for this slot, when set.                                  |
| `data.coachAvailability.availabilityPeriods[].weeklyPattern[].slots[].zoneKey`                   | string    | yes      | Location zone key, when set.                                          |
| `data.coachAvailability.availabilityPeriods[].weeklyPattern[].slots[].allowedEventTemplateIds[]` | string\[] | no       | Event templates that can derive bookable slots from this coach slot.  |
| `data.coachAvailability.specificAvailability[]`                                                  | array     | no       | Ad-hoc date-specific working slots.                                   |
| `data.coachAvailability.overrides[]`                                                             | array     | no       | Date-specific blocks such as day off, sick, or holiday.               |
| `data.coachAvailability.settings`                                                                | object    | no       | Booking-window, buffer, placement, and template fallback settings.    |
| `data.coachAvailability.createdAt`                                                               | string    | yes      | Creation timestamp.                                                   |
| `data.coachAvailability.updatedAt`                                                               | string    | yes      | Last update timestamp.                                                |

### Replace coach availability

```http theme={null}
PUT /public/v1/coaches/{coachId}/availability
Authorization: Bearer <access_token>
Content-Type: application/json
Idempotency-Key: <stable_request_key>
```

Required scope: `coach_availability:write`

Request body:

| Field                                           | Type   | Required | Rule                                                                                                                           |
| :---------------------------------------------- | :----- | :------- | :----------------------------------------------------------------------------------------------------------------------------- |
| `availabilityPeriods[]`                         | array  | no       | Full replacement date-ranged weekly templates.                                                                                 |
| `availabilityPeriods[].weeklyPattern[].slots[]` | array  | no       | Slot objects with `startTime`, `endTime`, optional `locationId`, optional `zoneKey`, and optional `allowedEventTemplateIds[]`. |
| `specificAvailability[]`                        | array  | no       | Full replacement ad-hoc date availability.                                                                                     |
| `overrides[]`                                   | array  | no       | Full replacement date blocks. Supported types are `DayOff`, `Sick`, and `Holiday`.                                             |
| `settings`                                      | object | no       | Full replacement booking-window and placement settings.                                                                        |

Unknown top-level fields are rejected. Send the coach ID in the path, not in the
body.

## Location availability

Location availability endpoints manage the availability template for one
location. This is separate from public location opening hours and separate from
the final bookable availability feed.

```http theme={null}
GET /public/v1/locations/{locationId}/availability
Authorization: Bearer <access_token>
```

Required scope: `location_availability:read`

Response fields:

| Field                                                                         | Type      | Nullable | Description                                                                |
| :---------------------------------------------------------------------------- | :-------- | :------- | :------------------------------------------------------------------------- |
| `data.locationAvailability`                                                   | object    | yes      | Location availability template, or `null` when none has been configured.   |
| `data.locationAvailability.locationId`                                        | string    | no       | Location ID from the path.                                                 |
| `data.locationAvailability.weeklyPattern[]`                                   | array     | no       | Weekly location availability rules.                                        |
| `data.locationAvailability.weeklyPattern[].dayOfWeek`                         | integer   | yes      | Day index, `0` for Sunday through `6` for Saturday.                        |
| `data.locationAvailability.weeklyPattern[].isDayOff`                          | boolean   | no       | Whether this weekday is blocked.                                           |
| `data.locationAvailability.weeklyPattern[].allowedEventTemplateIds[]`         | string\[] | no       | Day-level event template restrictions.                                     |
| `data.locationAvailability.weeklyPattern[].slots[]`                           | array     | no       | Location availability slots for the day.                                   |
| `data.locationAvailability.weeklyPattern[].slots[].startTime`                 | string    | no       | Slot start time, `HH:mm`.                                                  |
| `data.locationAvailability.weeklyPattern[].slots[].endTime`                   | string    | no       | Slot end time, `HH:mm`.                                                    |
| `data.locationAvailability.weeklyPattern[].slots[].allowedEventTemplateIds[]` | string\[] | no       | Slot-level event template restrictions.                                    |
| `data.locationAvailability.overrides[]`                                       | array     | no       | Date-specific day off, maintenance, or custom-hour overrides.              |
| `data.locationAvailability.settings`                                          | object    | no       | Booking-window, buffer, calendar-blocking, and template fallback settings. |
| `data.locationAvailability.createdAt`                                         | string    | yes      | Creation timestamp.                                                        |
| `data.locationAvailability.updatedAt`                                         | string    | yes      | Last update timestamp.                                                     |

### Replace location availability

```http theme={null}
PUT /public/v1/locations/{locationId}/availability
Authorization: Bearer <access_token>
Content-Type: application/json
Idempotency-Key: <stable_request_key>
```

Required scope: `location_availability:write`

Request body:

| Field                     | Type   | Required | Rule                                                                                             |
| :------------------------ | :----- | :------- | :----------------------------------------------------------------------------------------------- |
| `weeklyPattern[]`         | array  | no       | Full replacement weekly location availability template.                                          |
| `weeklyPattern[].slots[]` | array  | no       | Slot objects with `startTime`, `endTime`, and optional `allowedEventTemplateIds[]`.              |
| `overrides[]`             | array  | no       | Full replacement date overrides. Supported types are `DayOff`, `CustomHours`, and `Maintenance`. |
| `settings`                | object | no       | Full replacement booking-window, buffer, calendar-blocking, and template fallback settings.      |

Unknown top-level fields are rejected. Send the location ID in the path, not in
the body.

## Closure days and absence

These endpoints mirror the company calendar settings panel for availability
closures. They manage holiday presets and custom closure or absence dates that
block bookable availability for coaches, locations, or both. This is the Public
API surface for calendar closure days, not location opening hours.

```http theme={null}
GET /public/v1/availability-closures
Authorization: Bearer <access_token>
```

Required scope: `availability_rules:read`

Response fields:

| Field                                                                    | Type      | Nullable | Description                                       |
| :----------------------------------------------------------------------- | :-------- | :------- | :------------------------------------------------ |
| `data.availabilityClosures.presets[].key`                                | string    | no       | Preset key.                                       |
| `data.availabilityClosures.presets[].enabled`                            | boolean   | no       | Whether this holiday preset blocks availability.  |
| `data.availabilityClosures.presets[].appliesToCoaches`                   | boolean   | no       | Whether the preset blocks coach availability.     |
| `data.availabilityClosures.presets[].appliesToLocations`                 | boolean   | no       | Whether the preset blocks location availability.  |
| `data.availabilityClosures.presets[].holidayKeys[]`                      | string\[] | no       | Holiday keys selected from this preset.           |
| `data.availabilityClosures.presets[].coachIds[]`                         | string\[] | no       | Scoped coach IDs.                                 |
| `data.availabilityClosures.presets[].locationIds[]`                      | string\[] | no       | Scoped location IDs.                              |
| `data.availabilityClosures.customClosures[].key`                         | string    | no       | Closure key.                                      |
| `data.availabilityClosures.customClosures[].name`                        | string    | no       | Closure name.                                     |
| `data.availabilityClosures.customClosures[].date`                        | string    | no       | Closure date.                                     |
| `data.availabilityClosures.customClosures[].recurring`                   | boolean   | no       | Recurring yearly flag.                            |
| `data.availabilityClosures.customClosures[].enabled`                     | boolean   | no       | Whether this custom closure blocks availability.  |
| `data.availabilityClosures.customClosures[].reason`                      | string    | no       | Internal closure or absence note.                 |
| `data.availabilityClosures.customClosures[].appliesToCoaches`            | boolean   | no       | Whether the closure blocks coach availability.    |
| `data.availabilityClosures.customClosures[].appliesToLocations`          | boolean   | no       | Whether the closure blocks location availability. |
| `data.availabilityClosures.customClosures[].locationIds[]`               | string\[] | no       | Scoped location IDs.                              |
| `data.availabilityClosures.customClosures[].coachIds[]`                  | string\[] | no       | Scoped coach IDs.                                 |
| `data.availabilityClosures.availablePresets[].key`                       | string    | no       | Available preset key.                             |
| `data.availabilityClosures.availablePresets[].countryCode`               | string    | yes      | Preset country code.                              |
| `data.availabilityClosures.availablePresets[].regionCode`                | string    | yes      | Preset region code.                               |
| `data.availabilityClosures.availablePresets[].label.en`                  | string    | no       | English preset label.                             |
| `data.availabilityClosures.availablePresets[].label.nl`                  | string    | no       | Dutch preset label.                               |
| `data.availabilityClosures.availablePresets[].holidays[].key`            | string    | no       | Public holiday key.                               |
| `data.availabilityClosures.availablePresets[].holidays[].label.en`       | string    | no       | English holiday label.                            |
| `data.availabilityClosures.availablePresets[].holidays[].label.nl`       | string    | no       | Dutch holiday label.                              |
| `data.availabilityClosures.availablePresets[].holidays[].defaultEnabled` | boolean   | no       | Whether the holiday is enabled by default.        |
| `data.availabilityClosures.updatedAt`                                    | string    | yes      | Last update timestamp.                            |

### Replace closure days and absence

```http theme={null}
PUT /public/v1/availability-closures
Authorization: Bearer <access_token>
Content-Type: application/json
```

Required scope: `availability_rules:write`

Request body:

| Field              | Type  | Required | Rule                                                                                                    |
| :----------------- | :---- | :------- | :------------------------------------------------------------------------------------------------------ |
| `presets[]`        | array | no       | Full replacement holiday preset array accepted by the availability closure normalizer.                  |
| `customClosures[]` | array | no       | Full replacement custom closure and absence date array accepted by the availability closure normalizer. |

Unknown top-level fields are rejected. Because this is a full replace endpoint,
callers should first read the current closure document, apply their intended
change locally, and send the complete desired `presets` and `customClosures`
payload.

The update response returns `data.availabilityClosures.presets[]`,
`data.availabilityClosures.customClosures[]`,
`data.availabilityClosures.availablePresets[]`, and
`data.availabilityClosures.updatedAt`.

Not exposed in Public API v1: raw RRULE objects, external calendar sync state,
manual waitlist promotion controls, notification payloads, or internal
availability derivation metadata.

## List bookable availability

```http theme={null}
GET /public/v1/availability?dateFrom=2026-07-20T00:00:00.000Z&dateTo=2026-07-27T23:59:59.999Z&eventTemplateId={templateId}
Authorization: Bearer <access_token>
```

Required scope: `availability:read`

Bookable availability is the endpoint for external client booking flows. It
combines calendar events, coach availability templates, location availability
templates, closure days, booking rules, capacity, and optional client context.
It returns only currently bookable or waitlist-available slots and includes a
short-lived `availabilityToken`. The token is signed, company-scoped, expires
after 10 minutes, and is not a reservation. `POST /public/v1/bookings` always
revalidates capacity and booking eligibility live.

Validation:

| Parameter          | Type                   | Required | Rule                                                                                                                                  |
| :----------------- | :--------------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------ |
| `dateFrom`         | ISO date-time          | yes      | Valid date-time.                                                                                                                      |
| `dateTo`           | ISO date-time          | yes      | Valid date-time, must be after `dateFrom`.                                                                                            |
| `page`             | integer                | no       | Minimum `1`.                                                                                                                          |
| `limit`            | integer                | no       | Default `100`, maximum `100`.                                                                                                         |
| `clientId`         | string                 | no       | When supplied, client must belong to the authenticated company. Client product visibility and duplicate-booking state are considered. |
| `eventTemplateId`  | string                 | no       | Restrict to one event template.                                                                                                       |
| `eventTemplateIds` | comma-separated string | no       | Restrict to multiple event templates.                                                                                                 |
| `coachId`          | string                 | no       | Restrict to slots assigned to this coach.                                                                                             |
| `locationId`       | string                 | no       | Restrict to this location ID.                                                                                                         |

Range limit: maximum 31 days.

Response fields:

| Field                                    | Type    | Nullable | Description                                                                                        |
| :--------------------------------------- | :------ | :------- | :------------------------------------------------------------------------------------------------- |
| `data.dateFrom`                          | string  | no       | Parsed UTC range start.                                                                            |
| `data.dateTo`                            | string  | no       | Parsed UTC range end.                                                                              |
| `data.page`                              | integer | no       | Current page.                                                                                      |
| `data.limit`                             | integer | no       | Page size after cap.                                                                               |
| `data.total`                             | integer | no       | Total matching slots.                                                                              |
| `data.totalPages`                        | integer | no       | Total pages.                                                                                       |
| `data.hasNextPage`                       | boolean | no       | Next page availability.                                                                            |
| `data.hasPrevPage`                       | boolean | no       | Previous page availability.                                                                        |
| `data.slots[].slotId`                    | string  | no       | Opaque slot ID. May be a calendar occurrence ID or synthetic availability slot reference.          |
| `data.slots[].availabilityToken`         | string  | no       | Short-lived signed token for booking this exact slot.                                              |
| `data.slots[].calendarEventId`           | string  | yes      | Existing calendar event ID, or `null` for synthetic slots until booking creates the backing event. |
| `data.slots[].recurringMasterEventId`    | string  | yes      | Recurring master event ID when applicable.                                                         |
| `data.slots[].instanceDate`              | string  | yes      | Recurring occurrence date.                                                                         |
| `data.slots[].eventTemplateId`           | string  | yes      | Template ID.                                                                                       |
| `data.slots[].name`                      | string  | no       | Slot/event name.                                                                                   |
| `data.slots[].startAt`                   | string  | yes      | Slot start in UTC.                                                                                 |
| `data.slots[].endAt`                     | string  | yes      | Slot end in UTC.                                                                                   |
| `data.slots[].timeZone`                  | string  | no       | Display timezone used by the availability engine.                                                  |
| `data.slots[].coaches[]`                 | array   | no       | Public coach references.                                                                           |
| `data.slots[].coaches[].coachId`         | string  | yes      | Coach ID when visible.                                                                             |
| `data.slots[].coaches[].name`            | string  | no       | Coach display name when visible.                                                                   |
| `data.slots[].location.name`             | string  | no       | Location name.                                                                                     |
| `data.slots[].location.zone`             | string  | no       | Location zone.                                                                                     |
| `data.slots[].location.address`          | string  | no       | Location address when available.                                                                   |
| `data.slots[].location.longitude`        | number  | yes      | Longitude.                                                                                         |
| `data.slots[].location.latitude`         | number  | yes      | Latitude.                                                                                          |
| `data.slots[].capacity.total`            | number  | yes      | Total bookable seats, or `null` when unlimited/not exposed.                                        |
| `data.slots[].capacity.booked`           | number  | yes      | Booked seats.                                                                                      |
| `data.slots[].capacity.remaining`        | number  | yes      | Remaining seats.                                                                                   |
| `data.slots[].waitlist.available`        | boolean | no       | Whether waitlist booking is available.                                                             |
| `data.slots[].waitlist.booked`           | number  | yes      | Current waitlist count.                                                                            |
| `data.slots[].bookingMode`               | string  | no       | `bookable`, `waitlist_available`, `full`, or `unavailable`.                                        |
| `data.slots[].pricing.price`             | number  | yes      | Lesson price when configured.                                                                      |
| `data.slots[].pricing.currency`          | string  | no       | Currency code.                                                                                     |
| `data.slots[].pricing.vatPercentage`     | number  | yes      | VAT percentage.                                                                                    |
| `data.slots[].creditRequirement.credits` | number  | no       | Credits required for booking.                                                                      |

## List bookings

```http theme={null}
GET /public/v1/bookings?dateFrom=2026-07-01T00:00:00.000Z&dateTo=2026-07-31T23:59:59.999Z&limit=50
Authorization: Bearer <access_token>
```

Required scope: `bookings:read`

Validation:

| Parameter  | Type          | Required | Rule                                                                                                                          |
| :--------- | :------------ | :------- | :---------------------------------------------------------------------------------------------------------------------------- |
| `dateFrom` | ISO date-time | yes      | Valid date-time.                                                                                                              |
| `dateTo`   | ISO date-time | yes      | Valid date-time, max 92-day range.                                                                                            |
| `page`     | integer       | no       | Minimum `1`.                                                                                                                  |
| `limit`    | integer       | no       | Default `50`, maximum `50`.                                                                                                   |
| `status`   | string        | no       | `Planned`, `Waiting`, `RequestPending`, `CancelledByClient`, `CancelledByCoach`, `Completed`, or `BookingRequestNotAccepted`. |
| `clientId` | string        | no       | Valid client ObjectId connected to the company.                                                                               |
| `coachId`  | string        | no       | Valid coach ObjectId.                                                                                                         |

Response fields:

| Field                              | Type    | Nullable | Description                               |
| :--------------------------------- | :------ | :------- | :---------------------------------------- |
| `data.page`                        | integer | no       | Current page.                             |
| `data.limit`                       | integer | no       | Page size.                                |
| `data.total`                       | integer | no       | Total matching bookings.                  |
| `data.totalPages`                  | integer | no       | Total pages.                              |
| `data.hasNextPage`                 | boolean | no       | Next page availability.                   |
| `data.hasPrevPage`                 | boolean | no       | Previous page availability.               |
| `data.bookings[].bookingId`        | string  | no       | Booking ID.                               |
| `data.bookings[].status`           | string  | no       | Booking status.                           |
| `data.bookings[].attendanceStatus` | string  | no       | Attendance status.                        |
| `data.bookings[].client`           | object  | yes      | Client summary, omitted when unavailable. |
| `data.bookings[].client.name`      | string  | no       | Client display name.                      |
| `data.bookings[].event.name`       | string  | no       | Event name.                               |
| `data.bookings[].event.startAt`    | string  | yes      | Booking event start.                      |
| `data.bookings[].event.endAt`      | string  | yes      | Booking event end.                        |

<CodeGroup>
  ```bash cURL theme={null}
  curl -H "Authorization: Bearer $FITSOCIETY_ACCESS_TOKEN" \
    "https://api.fitsociety.io/public/v1/bookings?dateFrom=2026-07-01T00:00:00.000Z&dateTo=2026-07-31T23:59:59.999Z&status=Planned&limit=50"
  ```

  ```javascript JavaScript theme={null}
  const query = new URLSearchParams({
    dateFrom: "2026-07-01T00:00:00.000Z",
    dateTo: "2026-07-31T23:59:59.999Z",
    status: "Planned",
    limit: "50",
  });

  const response = await fetch(
    `https://api.fitsociety.io/public/v1/bookings?${query}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.FITSOCIETY_ACCESS_TOKEN}`,
      },
    },
  );

  const body = await response.json();
  console.log(body.data.total, body.data.bookings);
  ```

  ```python Python theme={null}
  import os
  import requests

  response = requests.get(
      "https://api.fitsociety.io/public/v1/bookings",
      headers={"Authorization": f"Bearer {os.environ['FITSOCIETY_ACCESS_TOKEN']}"},
      params={
          "dateFrom": "2026-07-01T00:00:00.000Z",
          "dateTo": "2026-07-31T23:59:59.999Z",
          "status": "Planned",
          "limit": 50,
      },
  )
  response.raise_for_status()

  body = response.json()
  print(body["data"]["total"], body["data"]["bookings"])
  ```
</CodeGroup>

Response:

```json theme={null}
{
  "data": {
    "page": 1,
    "limit": 50,
    "total": 1,
    "totalPages": 1,
    "hasNextPage": false,
    "hasPrevPage": false,
    "bookings": [
      {
        "bookingId": "64b64c0f2f5f4c0012345680",
        "status": "Planned",
        "attendanceStatus": "unknown",
        "client": { "name": "Jane Doe" },
        "event": {
          "name": "Small group training",
          "startAt": "2026-07-21T08:00:00.000Z",
          "endAt": "2026-07-21T09:00:00.000Z"
        }
      }
    ]
  },
  "meta": {
    "requestId": "req_0123456789abcdef",
    "rateLimit": { "limit": 10, "remaining": 9, "resetSeconds": 1 }
  }
}
```

## Get booking

```http theme={null}
GET /public/v1/bookings/{bookingId}
Authorization: Bearer <access_token>
```

Required scope: `bookings:read`

Response fields:

| Field                                  | Type    | Nullable | Description                                  |
| :------------------------------------- | :------ | :------- | :------------------------------------------- |
| `data.booking.bookingId`               | string  | no       | Booking ID.                                  |
| `data.booking.status`                  | string  | no       | Booking status.                              |
| `data.booking.attendanceStatus`        | string  | no       | Attendance status.                           |
| `data.booking.paymentMethod`           | string  | no       | Booking payment method.                      |
| `data.booking.paymentProcessingStatus` | string  | no       | Payment processing status.                   |
| `data.booking.usedCredits`             | number  | no       | Credits used by this booking.                |
| `data.booking.createdByType`           | string  | no       | Creator type.                                |
| `data.booking.isRecurringInstance`     | boolean | no       | Whether this belongs to a recurring booking. |
| `data.booking.recurringBookingId`      | string  | yes      | Recurring booking ID.                        |
| `data.booking.client.clientId`         | string  | no       | Client ID.                                   |
| `data.booking.client.name`             | string  | no       | Client display name.                         |
| `data.booking.event.eventId`           | string  | yes      | Event ID.                                    |
| `data.booking.event.name`              | string  | no       | Event name.                                  |
| `data.booking.event.startAt`           | string  | yes      | Event start.                                 |
| `data.booking.event.endAt`             | string  | yes      | Event end.                                   |
| `data.booking.event.instanceDate`      | string  | yes      | Recurring instance date.                     |

## Create booking

```http theme={null}
POST /public/v1/bookings
Authorization: Bearer <access_token>
Content-Type: application/json
```

Required scope: `bookings:write`

Request body:

| Field               | Type   | Required    | Rule                                                                                                                                  |
| :------------------ | :----- | :---------- | :------------------------------------------------------------------------------------------------------------------------------------ |
| `clientId`          | string | yes         | Client must belong to the authenticated company.                                                                                      |
| `availabilityToken` | string | conditional | Preferred. Token from `GET /public/v1/availability`. When supplied, caller-provided `calendarEventId` and `instanceDate` are ignored. |
| `calendarEventId`   | string | conditional | Required only when `availabilityToken` is not supplied. Event must belong to the authenticated company.                               |
| `instanceDate`      | string | no          | ISO date-time for recurring occurrence bookings when booking directly by event ID.                                                    |
| `paymentMethod`     | string | no          | `product` or `free`. Defaults to `product`.                                                                                           |
| `creditsToDeduct`   | number | no          | Only used for `product` bookings.                                                                                                     |

Availability-token booking validation:

| Validation         | Behavior                                                                                   |
| :----------------- | :----------------------------------------------------------------------------------------- |
| Token signature    | Invalid signatures return `400 PUBLIC_API_AVAILABILITY_TOKEN_INVALID_SIGNATURE`.           |
| Token company      | Company mismatch returns `403 PUBLIC_API_AVAILABILITY_TOKEN_COMPANY_MISMATCH`.             |
| Token expiry       | Expired tokens return `400 PUBLIC_API_AVAILABILITY_TOKEN_EXPIRED`.                         |
| Slot state         | Slot is loaded again before booking; stale full/unavailable slots return a client error.   |
| Synthetic slots    | Synthetic availability slots create the backing calendar event only after live validation. |
| Duplicate bookings | Existing booking controller blocks duplicate enrolment.                                    |
| Capacity           | Existing booking controller enforces group-size and waitlist rules.                        |

Response fields:

| Field                                  | Type    | Nullable | Description                                  |
| :------------------------------------- | :------ | :------- | :------------------------------------------- |
| `data.booking.bookingId`               | string  | no       | Booking ID.                                  |
| `data.booking.status`                  | string  | no       | Booking status.                              |
| `data.booking.attendanceStatus`        | string  | no       | Attendance status.                           |
| `data.booking.paymentMethod`           | string  | no       | Booking payment method.                      |
| `data.booking.paymentProcessingStatus` | string  | no       | Payment processing status.                   |
| `data.booking.usedCredits`             | number  | no       | Credits used by this booking.                |
| `data.booking.createdByType`           | string  | no       | Creator type.                                |
| `data.booking.isRecurringInstance`     | boolean | no       | Whether this belongs to a recurring booking. |
| `data.booking.recurringBookingId`      | string  | yes      | Recurring booking ID.                        |
| `data.booking.client.clientId`         | string  | no       | Client ID.                                   |
| `data.booking.client.name`             | string  | no       | Client display name.                         |
| `data.booking.event.eventId`           | string  | yes      | Event ID.                                    |
| `data.booking.event.name`              | string  | no       | Event name.                                  |
| `data.booking.event.startAt`           | string  | yes      | Event start.                                 |
| `data.booking.event.endAt`             | string  | yes      | Event end.                                   |
| `data.booking.event.instanceDate`      | string  | yes      | Recurring instance date.                     |

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.fitsociety.io/public/v1/bookings" \
    -H "Authorization: Bearer $FITSOCIETY_ACCESS_TOKEN" \
    -H "Idempotency-Key: booking-create-jane-20260721-0800" \
    -H "Content-Type: application/json" \
    -d '{
      "clientId": "64b64c0f2f5f4c0012345678",
      "availabilityToken": "<availabilityToken from GET /public/v1/availability>",
      "paymentMethod": "product"
    }'
  ```

  ```javascript JavaScript theme={null}
  const headers = {
    Authorization: `Bearer ${process.env.FITSOCIETY_ACCESS_TOKEN}`,
  };

  // 1. Fetch a bookable slot with its short-lived availabilityToken.
  const availabilityQuery = new URLSearchParams({
    dateFrom: "2026-07-20T00:00:00.000Z",
    dateTo: "2026-07-27T23:59:59.999Z",
    eventTemplateId: "64b64c0f2f5f4c0012345682",
  });
  const availability = await fetch(
    `https://api.fitsociety.io/public/v1/availability?${availabilityQuery}`,
    { headers },
  ).then((res) => res.json());
  const slot = availability.data.slots[0];

  // 2. Book the slot. Availability tokens are not reservations, so book quickly.
  const response = await fetch("https://api.fitsociety.io/public/v1/bookings", {
    method: "POST",
    headers: {
      ...headers,
      "Idempotency-Key": "booking-create-jane-20260721-0800",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      clientId: "64b64c0f2f5f4c0012345678",
      availabilityToken: slot.availabilityToken,
      paymentMethod: "product",
    }),
  });

  const body = await response.json();
  if (response.status === 201) {
    console.log("Created booking", body.data.booking.bookingId);
  }
  ```

  ```python Python theme={null}
  import os
  import requests

  headers = {"Authorization": f"Bearer {os.environ['FITSOCIETY_ACCESS_TOKEN']}"}

  # 1. Fetch a bookable slot with its short-lived availabilityToken.
  availability = requests.get(
      "https://api.fitsociety.io/public/v1/availability",
      headers=headers,
      params={
          "dateFrom": "2026-07-20T00:00:00.000Z",
          "dateTo": "2026-07-27T23:59:59.999Z",
          "eventTemplateId": "64b64c0f2f5f4c0012345682",
      },
  ).json()
  slot = availability["data"]["slots"][0]

  # 2. Book the slot. Availability tokens are not reservations, so book quickly.
  response = requests.post(
      "https://api.fitsociety.io/public/v1/bookings",
      headers={**headers, "Idempotency-Key": "booking-create-jane-20260721-0800"},
      json={
          "clientId": "64b64c0f2f5f4c0012345678",
          "availabilityToken": slot["availabilityToken"],
          "paymentMethod": "product",
      },
  )
  response.raise_for_status()

  body = response.json()
  if response.status_code == 201:
      print("Created booking", body["data"]["booking"]["bookingId"])
  ```
</CodeGroup>

Response (`201 Created`):

```json theme={null}
{
  "data": {
    "booking": {
      "bookingId": "64b64c0f2f5f4c0012345680",
      "status": "Planned",
      "attendanceStatus": "unknown",
      "paymentMethod": "product",
      "paymentProcessingStatus": "none",
      "usedCredits": 1,
      "createdByType": "public_api",
      "isRecurringInstance": false,
      "recurringBookingId": null,
      "client": {
        "clientId": "64b64c0f2f5f4c0012345678",
        "name": "Jane Doe"
      },
      "event": {
        "eventId": "64b64c0f2f5f4c0012345681",
        "name": "Small group training",
        "startAt": "2026-07-21T08:00:00.000Z",
        "endAt": "2026-07-21T09:00:00.000Z",
        "instanceDate": null
      }
    }
  },
  "meta": {
    "requestId": "req_0123456789abcdef",
    "rateLimit": { "limit": 10, "remaining": 8, "resetSeconds": 1 }
  }
}
```

## Update booking attendance

```http theme={null}
PATCH /public/v1/bookings/{bookingId}
Authorization: Bearer <access_token>
Content-Type: application/json
```

Required scope: `bookings:write`

Request body:

| Field          | Type   | Required | Rule                                   |
| :------------- | :----- | :------- | :------------------------------------- |
| `status`       | string | yes      | `attended`, `noshow`, or `unknown`.    |
| `instanceDate` | string | no       | ISO date-time for recurring instances. |
| `noShowReason` | string | no       | Max 500 characters.                    |
| `remarks`      | string | no       | Max 1000 characters.                   |

Response fields:

| Field                             | Type    | Nullable | Description                               |
| :-------------------------------- | :------ | :------- | :---------------------------------------- |
| `data.attendance.calendarEventId` | string  | yes      | Event ID.                                 |
| `data.attendance.clientId`        | string  | yes      | Client ID.                                |
| `data.attendance.instanceDate`    | string  | yes      | Instance date.                            |
| `data.attendance.status`          | string  | no       | Applied status.                           |
| `data.attendance.attended`        | boolean | no       | `true` only for `attended`.               |
| `data.attendance.updatedAt`       | string  | yes      | Update timestamp.                         |
| `data.attendance.updatedBy.id`    | string  | yes      | Acting coach ID.                          |
| `data.attendance.updatedBy.name`  | string  | no       | Acting coach display name when available. |

## Get booking cancellation context

```http theme={null}
GET /public/v1/bookings/{bookingId}/cancellation-context
Authorization: Bearer <access_token>
```

Required scope: `bookings:read`

Optional query: `clientId`, `instanceDate`.

Response fields:

| Field                                                        | Type      | Nullable | Description                                          |
| :----------------------------------------------------------- | :-------- | :------- | :--------------------------------------------------- |
| `data.cancellationContext.bookingId`                         | string    | no       | Booking ID.                                          |
| `data.cancellationContext.clientId`                          | string    | no       | Client ID.                                           |
| `data.cancellationContext.calendarEventId`                   | string    | no       | Event ID.                                            |
| `data.cancellationContext.lessonId`                          | string    | no       | Lesson/template ID.                                  |
| `data.cancellationContext.occurrence.isRecurring`            | boolean   | no       | Whether cancellation targets a recurring occurrence. |
| `data.cancellationContext.occurrence.masterEventId`          | string    | yes      | Master event ID.                                     |
| `data.cancellationContext.occurrence.exceptionEventId`       | string    | yes      | Exception event ID.                                  |
| `data.cancellationContext.occurrence.instanceDate`           | string    | yes      | Instance date.                                       |
| `data.cancellationContext.timing.startDate`                  | string    | yes      | Event start.                                         |
| `data.cancellationContext.timing.endDate`                    | string    | yes      | Event end.                                           |
| `data.cancellationContext.timing.isPast`                     | boolean   | no       | Whether the event is in the past.                    |
| `data.cancellationContext.policy.defaultFinancialAction`     | string    | no       | Default action based on cancellation policy.         |
| `data.cancellationContext.financial.availableActions[]`      | string\[] | no       | Allowed financial actions.                           |
| `data.cancellationContext.financial.chargeAllowed`           | boolean   | no       | Whether a charge action can be applied.              |
| `data.cancellationContext.financial.hasCreditUsage`          | boolean   | no       | Whether credits were used.                           |
| `data.cancellationContext.financial.creditsUsed`             | number    | no       | Credits used.                                        |
| `data.cancellationContext.financial.paymentMethod`           | string    | no       | Booking payment method.                              |
| `data.cancellationContext.financial.paymentProcessingStatus` | string    | no       | Payment processing status.                           |
| `data.cancellationContext.financial.paymentTransactionId`    | string    | no       | Payment transaction reference when present.          |
| `data.cancellationContext.reasons[].code`                    | string    | no       | Configured cancellation reason code.                 |
| `data.cancellationContext.reasons[].label`                   | string    | no       | Localized cancellation reason label.                 |
| `data.cancellationContext.reasons[].labels`                  | object    | no       | Localized labels keyed by language.                  |
| `data.cancellationContext.reasons[].isDefault`               | boolean   | no       | Default reason flag.                                 |
| `data.cancellationContext.reasons[].active`                  | boolean   | no       | Active reason flag.                                  |
| `data.cancellationContext.reasons[].requiresText`            | boolean   | no       | Whether extra text is required.                      |
| `data.cancellationContext.reasons[].sortOrder`               | number    | no       | Reason sort order.                                   |
| `data.cancellationContext.requiresReason`                    | boolean   | no       | Whether reason text/code is required.                |

## Cancel booking

```http theme={null}
POST /public/v1/bookings/{bookingId}/cancel
Authorization: Bearer <access_token>
Content-Type: application/json
```

Required scope: `bookings:write`

Request body:

| Field                    | Type    | Required    | Rule                                                                         |
| :----------------------- | :------ | :---------- | :--------------------------------------------------------------------------- |
| `clientId`               | string  | no          | Extra client guard.                                                          |
| `instanceDate`           | string  | no          | Required by some recurring occurrence cancellations.                         |
| `financialAction`        | string  | no          | `followPolicy`, `refund`, `retain`, or `charge`. Defaults to `followPolicy`. |
| `overridePolicy`         | boolean | no          | Applies only when explicitly true.                                           |
| `chargeClientCreditId`   | string  | conditional | Required when `financialAction` resolves to `charge`.                        |
| `chargeCredits`          | number  | conditional | Required when charging credits.                                              |
| `cancellationReasonCode` | string  | no          | Must match a configured reason when supplied.                                |
| `cancellationReasonText` | string  | no          | Custom reason text.                                                          |
| `coachNote`              | string  | no          | Internal/client note used in notifications/history.                          |
| `notifyClient`           | boolean | no          | Defaults to `false` in Public API.                                           |
| `notifyCoach`            | boolean | no          | Defaults to `false`.                                                         |
| `promoteWaitlist`        | boolean | no          | Defaults to `true`.                                                          |

Response fields:

| Field                                                | Type    | Nullable | Description                                  |
| :--------------------------------------------------- | :------ | :------- | :------------------------------------------- |
| `data.bookingId`                                     | string  | no       | Booking ID.                                  |
| `data.bookingStatus`                                 | string  | no       | Final booking status.                        |
| `data.status`                                        | string  | no       | Final legacy status.                         |
| `data.alreadyCancelled`                              | boolean | no       | Whether the booking was already cancelled.   |
| `data.financial.actionApplied`                       | string  | no       | `refund`, `retain`, or `charge`.             |
| `data.financial.creditsRefunded`                     | number  | no       | Refunded credits.                            |
| `data.financial.singleSessionReset`                  | boolean | no       | Whether a single-session credit was reset.   |
| `data.financial.creditsCharged`                      | number  | no       | Charged credits.                             |
| `data.financial.chargedClientCreditId`               | string  | yes      | Credit ID used for charge.                   |
| `data.financial.chargeCreditUsages[].clientCreditId` | string  | no       | Client credit ID charged.                    |
| `data.financial.chargeCreditUsages[].creditsUsed`    | number  | no       | Credits charged from this credit record.     |
| `data.financial.chargeCreditUsages[].usageType`      | string  | no       | Usage type.                                  |
| `data.financial.chargeCreditUsages[].creditPoolKey`  | string  | no       | Credit pool key.                             |
| `data.financial.chargeCreditUsages[].creditPoolName` | string  | no       | Credit pool name.                            |
| `data.financial.chargeCreditUsages[].creditPoolMode` | string  | no       | Credit pool mode.                            |
| `data.policy.policy`                                 | string  | no       | Cancellation policy key used for evaluation. |
| `data.policy.customCancellationPolicyHours`          | number  | no       | Custom policy window in hours.               |
| `data.policy.eventStartAt`                           | string  | yes      | Event start used for policy evaluation.      |
| `data.policy.evaluatedAt`                            | string  | no       | Policy evaluation timestamp.                 |
| `data.policy.refundEligible`                         | boolean | no       | Whether policy allowed a refund.             |
| `data.policy.overridePolicy`                         | boolean | no       | Whether the request overrode policy.         |

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.fitsociety.io/public/v1/bookings/64b64c0f2f5f4c0012345680/cancel" \
    -H "Authorization: Bearer $FITSOCIETY_ACCESS_TOKEN" \
    -H "Idempotency-Key: booking-cancel-64b64c0f2f5f4c0012345680" \
    -H "Content-Type: application/json" \
    -d '{
      "financialAction": "followPolicy",
      "cancellationReasonText": "Client is ill",
      "notifyClient": true
    }'
  ```

  ```javascript JavaScript theme={null}
  const bookingId = "64b64c0f2f5f4c0012345680";
  const response = await fetch(
    `https://api.fitsociety.io/public/v1/bookings/${bookingId}/cancel`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.FITSOCIETY_ACCESS_TOKEN}`,
        "Idempotency-Key": `booking-cancel-${bookingId}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        financialAction: "followPolicy",
        cancellationReasonText: "Client is ill",
        notifyClient: true,
      }),
    },
  );

  const body = await response.json();
  console.log(body.data.bookingStatus, body.data.financial.actionApplied);
  ```

  ```python Python theme={null}
  import os
  import requests

  booking_id = "64b64c0f2f5f4c0012345680"
  response = requests.post(
      f"https://api.fitsociety.io/public/v1/bookings/{booking_id}/cancel",
      headers={
          "Authorization": f"Bearer {os.environ['FITSOCIETY_ACCESS_TOKEN']}",
          "Idempotency-Key": f"booking-cancel-{booking_id}",
      },
      json={
          "financialAction": "followPolicy",
          "cancellationReasonText": "Client is ill",
          "notifyClient": True,
      },
  )
  response.raise_for_status()

  body = response.json()
  print(body["data"]["bookingStatus"], body["data"]["financial"]["actionApplied"])
  ```
</CodeGroup>

Response:

```json theme={null}
{
  "data": {
    "bookingId": "64b64c0f2f5f4c0012345680",
    "bookingStatus": "CancelledByCoach",
    "status": "CancelledByCoach",
    "alreadyCancelled": false,
    "financial": {
      "actionApplied": "refund",
      "creditsRefunded": 1,
      "singleSessionReset": false,
      "creditsCharged": 0,
      "chargedClientCreditId": null,
      "chargeCreditUsages": []
    },
    "policy": {
      "policy": "flexible",
      "customCancellationPolicyHours": 24,
      "eventStartAt": "2026-07-21T08:00:00.000Z",
      "evaluatedAt": "2026-07-20T09:15:00.000Z",
      "refundEligible": true,
      "overridePolicy": false
    }
  },
  "meta": {
    "requestId": "req_0123456789abcdef",
    "rateLimit": { "limit": 10, "remaining": 7, "resetSeconds": 1 }
  }
}
```
