# Update one or more booking sheet exceptions

`PUT /api/booking-sheets/exception/{scheduleId}`

- **Success:** `200 OK`
- **Products:** Operator (`FOH`)
- **Features:** Front of House Generic Access (`FOHGENERIC`)

HTML version: [Update one or more booking sheet exceptions](https://support.gladstonesoftware.cloud/reference/bookings/update-one-or-more-booking-sheet-exceptions)

## Overview

Rewrites the exceptions produced by an existing recurring schedule. An **exception** blocks a
resource product for a period of time so that no booking can be taken against it — a maintenance
window, a staff meeting, a closure. A **schedule** is the recurrence definition that generates
those exceptions, identified by the GUID returned in `scheduleId` when you created it with
[Create one or more booking sheet exceptions](create-one-or-more-booking-sheet-exceptions.md).

Send the schedule's definition in full. The request body is the same shape as the create call, and
it replaces the stored definition outright: the schedule's future exception records are deleted and
regenerated from the `times` and recurrence fields you send. Records that have already finished are
left alone, so past exceptions survive an update. Anything you omit — days, notes, exclusion
dates — is not carried over from the previous definition.

The endpoint expands your `times` into individual occurrences, one per date per entry in
`productIds`, and checks them for clashes before writing anything. Nothing is written unless every
check passes; there is no partial update.

Use this endpoint to:

- move or resize a closure that has already been scheduled
- change which resource products a schedule blocks out
- change the recurrence pattern, for example from every week to every other week
- add dates to skip, through `exclusionDates`

**Read `errors` and `exceptionsUpdated` on every `200 OK`.** A clash sends back `400` (your own
occurrences overlap each other) or `409` (they overlap existing bookings or other schedules'
exceptions), in both cases with the full response body so you can see what collided. But if the
recurrence produces no dates at all — for example `days` names a weekday that never falls inside
the date range — the call succeeds with `200 OK`, `exceptionsUpdated: 0`, an empty `scheduleId` and
`errors` explaining why, and nothing has changed. Treating any `200` as success will leave you
believing a closure is in place when it is not.

## Endpoint

`PUT {base_url}/api/booking-sheets/exception/{scheduleId}`

`{base_url}` is the environment URL issued to your integration. See [Authenticating with Gladstone APIs](../../guides/authenticating-with-gladstone-apis.md).

## Path parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `scheduleId` | string | Yes | The schedule whose exceptions you are replacing, as a GUID in `8-4-4-4-12` form. Use the `scheduleId` returned by [Create one or more booking sheet exceptions](create-one-or-more-booking-sheet-exceptions.md). An empty value is rejected with `Schedule ID must be specified`, a value that does not parse as a GUID with `Schedule ID is not a valid GUID`, and a well-formed GUID with no exception records against it with `404` and `No exceptions found for Schedule ID '{scheduleId}'`. |

## Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `siteId` | string | Yes | The site (leisure centre or venue) the exceptions belong to. Missing or empty is rejected with `Site ID must be specified`; a value that matches no site with `Invalid Site ID '{siteId}'`. |
| `productIds` | array of string | Yes | The resource products to block — the bookable items on the booking sheet, such as a court, lane or studio. Every occurrence the recurrence generates is written once for each product listed, so all of them are blocked over the same pattern. At least one is required (`One or more Resource Products must be specified`, or `No Product IDs found in request` for an empty array), the list must contain no duplicates (`Duplicate Product IDs found in request`), and every id must be an existing resource product (`One or more Product IDs are invalid '{ids}'`). |
| `days` | array of string | No | Restricts the generated occurrences to particular weekdays. Use full English day names, case-insensitive — `monday` through `sunday`; abbreviations and anything else are rejected with `Invalid day in request. Valid days are: {list}`, and repeats with `Duplicate Days found in request`. Omit it, or send an empty array, to allow every day. What is permitted depends on `frequency`: at least one day is required for `Weekly` (`At least one day must be specified for Weekly frequency`), exactly one for `Monthly` with `recurranceType` `ByDay`, and none at all for `Monthly` with `ByDate` (`Days cannot be specified when RecurranceType is 'ByDate'`). |
| `times` | array of object | Yes | The time bands to block, each with the window over which to repeat. The date parts of an entry bound the recurrence; the time-of-day parts set the start and end of each occurrence generated within it. At least one entry is required — `One or more Exception Times must be specified` for a missing or empty array, `At least one Exception Time must be specified` for a null one. This list replaces the schedule's existing times outright. |
| `times[].timeBandId` | string | Yes | The time band to block: a named period of the day configured against the booking sheet. Missing or empty is rejected with `Timeband ID must be specified`. The band must exist **and** be configured as entry-restricted, otherwise the request fails with `One or more TimeBand IDs do not exist or are not marked as entry restricted '{ids}'` — a band that exists but is not entry-restricted produces the same message as one that does not exist at all. |
| `times[].dateTimeFrom` | string (date-time) | No | The start of the window, and the start time of day of each generated occurrence. Marked optional, but omitting it defaults to `0001-01-01T00:00:00`, which is rejected with `Invalid DateTimeFrom or DateTimeTo for time band {timeBandId}`. It must be in the future — `DateTimeFrom ({date}) cannot be in the past for time band {timeBandId}`. Seconds are rounded to the nearest second before validation. |
| `times[].dateTimeTo` | string (date-time) | No | The end of the window, and the end time of day of each generated occurrence. Marked optional, but rejected with `Invalid DateTimeFrom or DateTimeTo for time band {timeBandId}` when omitted. It must be later than `dateTimeFrom` (`DateTimeTo ({to}) must be after DateTimeFrom ({from}) for time band {timeBandId}`) and its **time of day** must also be later than that of `dateTimeFrom` (`DateTimeTo time ({hh:mm}) must be after DateTimeFrom time ({hh:mm}) for time band {timeBandId}`), so a single occurrence cannot cross midnight — split an overnight closure into one entry per day. Each stored occurrence ends one second before this time of day, so consecutive blocks do not register as overlapping. |
| `notes` | string | No | Free-text note copied onto every exception this request generates, for whoever looks at the booking sheet later. An empty string is saved as `null`. |
| `frequency` | string | No | How the window in each `times` entry is expanded into occurrences: `Daily`, `Weekly` or `Monthly`, matched case-insensitively. Defaults to `Daily` when omitted; any other value is rejected with `Invalid Frequency '{value}', must be one of 'Daily', 'Weekly', 'Monthly'`. It also decides which of `days`, `recursEvery`, `recurranceType` and `recursOn` are allowed — each of those is rejected outright under the wrong frequency, rather than ignored. |
| `recursEvery` | integer (int32) | No | The gap between occurrences: weeks for `Weekly`, months for `Monthly`. Defaults to `1` for those two frequencies, and must be greater than zero (`RecursEvery must be greater than zero`). For `Daily` it must be omitted or `1` — any other value is rejected with `RecursEvery cannot be specified for Daily frequency`. |
| `recurranceType` | string | No | For `Monthly` only, how the monthly date is chosen — note the spelling of the field. `ByDate` repeats on the same day of the month as the first `times` entry's `dateTimeFrom`, clamped to the last day in shorter months; `ByDay` repeats on the nth named weekday, combining `recursOn` with the single entry in `days`. Matched case-insensitively; an unrecognised value gives `Invalid RecurranceType '{value}', must be one of 'ByDate', 'ByDay'`. Required for `Monthly` (`RecurranceType must be specified for Monthly frequency`) and forbidden otherwise (`RecurranceType cannot be specified for Daily frequency`, `... for Weekly frequency`). |
| `recursOn` | integer (int32) | No | Which occurrence of the weekday within the month, for `Monthly` with `recurranceType` `ByDay` — `3` together with `days: ["friday"]` means the third Friday. Must be between 1 and 5 (`RecursOn must be between 1 and 5`) and is required in that combination (`RecursOn must be specified when RecurranceType is 'ByDay'`). It is rejected everywhere else: `RecursOn cannot be specified when RecurranceType is 'ByDate'`, `RecursOn cannot be specified for Daily frequency`, `RecursOn cannot be specified for Weekly frequency`. |
| `exclusionDates` | array of string | No | Dates to skip. Any generated occurrence falling on one of these dates is dropped; only the date part is compared, so the time you send is irrelevant. Note that [Delete booking sheet exceptions within a date range](delete-booking-sheet-exceptions-within-a-date-range.md) appends the deleted dates to the stored schedule's exclusion list — because this call overwrites the stored definition, omitting `exclusionDates` here silently reinstates dates that were previously deleted. |
| `additionalDates` | array of string | No | Accepted and stored with the schedule definition, but no occurrences are generated from it — only `times` combined with the recurrence fields produce exception records. To block a one-off extra date, add another entry to `times`. |

## Sample request

```bash title="cURL"
curl -X PUT '{base_url}/api/booking-sheets/exception/{scheduleId}' \
  -H 'Authorization: Bearer {accessToken}' \
  -H 'X-Use-SSO: true' \
  -H 'Content-Type: application/json' \
  -d '{
  "siteId": "string",
  "productIds": [
    "string"
  ],
  "days": [
    "string"
  ],
  "times": [
    {
      "timeBandId": "string",
      "dateTimeFrom": "2026-08-14T19:00:00Z",
      "dateTimeTo": "2026-08-14T19:00:00Z"
    }
  ],
  "notes": "string",
  "frequency": "string",
  "recursEvery": 1,
  "recurranceType": "string",
  "recursOn": 1,
  "exclusionDates": [
    "2026-08-14T19:00:00Z"
  ],
  "additionalDates": [
    "2026-08-14T19:00:00Z"
  ]
}'
```

## Response fields

| Field | Type | Description |
|-------|------|-------------|
| `exceptionsUpdated` | integer (int32) | How many exception records were written — one per generated date per entry in `productIds`, so a fortnight of weekday closures over three products is 30. `0` means nothing was written and the schedule is unchanged; check `errors` when you see it. |
| `scheduleId` | string (uuid) | The schedule the new exception records belong to. It is the same GUID you sent in the path when records were written, and `00000000-0000-0000-0000-000000000000` when none were. |
| `errors` | array of string | Why the update did not proceed. Empty on a successful update. The possible entries are `No valid dates found for the specified days`, `One or more exceptions overlap with each other`, `One or more exceptions overlap with existing exceptions for the same product(s)` and `One or more exceptions overlap with existing bookings for the same product(s)`. If this array is non-empty, nothing was written — including on a `200 OK`, which is what you get when the only entry is `No valid dates found for the specified days`. |
| `overlappingExceptions` | array of object | Occurrences generated from your own request that collide with each other — for example two `times` entries covering the same time band on the same day. Accompanies a `400`, and is empty otherwise. Fix the request; no existing data is involved. |
| `overlappingExceptions[].dateTimeFrom` | string (date-time) | Start of the generated occurrence that was found to overlap another one in the same request. |
| `overlappingExceptions[].dateTimeTo` | string (date-time) | End of that occurrence — the time band's end time of day, less one second. |
| `overlappingExceptions[].timeBandId` | string | The time band the overlapping occurrence was generated for. |
| `overlappingExceptions[].productId` | string | Always null here. Self-overlap is detected on the generated dates before they are multiplied out across `productIds`, so no single product can be named; the collision applies to every product in the request. |
| `existingExceptions` | array of object | Exceptions already stored against the same resource products that overlap the occurrences you asked for. Accompanies a `409`, and is empty otherwise. Exceptions belonging to this schedule are excluded, so a schedule never clashes with the records it is replacing — anything listed here belongs to a different schedule and must be dealt with before the update can succeed. |
| `existingExceptions[].dateTimeFrom` | string (date-time) | Start of the exception already in place. |
| `existingExceptions[].dateTimeTo` | string (date-time) | End of the exception already in place. |
| `existingExceptions[].timeBandId` | string | The time band the existing exception was recorded against. |
| `existingExceptions[].productId` | string | The resource product the existing exception blocks — one of the ids you sent in `productIds`. |
| `existingBookings` | array of object | Bookings already taken against the same resource products that overlap the occurrences you asked for. Accompanies a `409`, and is empty otherwise. The update cannot proceed over a live booking: cancel or move the bookings listed here, then retry. |
| `existingBookings[].bookingId` | integer (int32) | The id of the clashing booking, for looking it up or cancelling it. |
| `existingBookings[].dateTimeFrom` | string (date-time) | Start of the clashing booking. |
| `existingBookings[].dateTimeTo` | string (date-time) | End of the clashing booking. |
| `existingBookings[].timeBandId` | string | Always null for bookings. A booking is recorded against a date and time rather than a time band, so this field is not populated in this array. |
| `existingBookings[].productId` | string | The resource product the clashing booking occupies — one of the ids you sent in `productIds`. |

## Sample response

```json title="200 OK"
{
  "exceptionsUpdated": 1,
  "scheduleId": "b7f3c1e2-9a44-4f2b-8c17-0d5e9a1b6c33",
  "errors": [
    "string"
  ],
  "overlappingExceptions": [
    {
      "dateTimeFrom": "2026-08-14T19:00:00Z",
      "dateTimeTo": "2026-08-14T19:00:00Z",
      "timeBandId": "string",
      "productId": "string"
    }
  ],
  "existingExceptions": [
    {
      "dateTimeFrom": "2026-08-14T19:00:00Z",
      "dateTimeTo": "2026-08-14T19:00:00Z",
      "timeBandId": "string",
      "productId": "string"
    }
  ],
  "existingBookings": [
    {
      "bookingId": 12345,
      "dateTimeFrom": "2026-08-14T19:00:00Z",
      "dateTimeTo": "2026-08-14T19:00:00Z",
      "timeBandId": "string",
      "productId": "string"
    }
  ]
}
```

## Errors

| Status | Meaning | When |
|--------|---------|------|
| `400` | Bad request | The path parameter is unusable: `Schedule ID must be specified` or `Schedule ID is not a valid GUID` |
| `400` | Bad request | A required value is missing: `Site ID must be specified`, `One or more Resource Products must be specified`, `No Product IDs found in request`, `One or more Exception Times must be specified`, `At least one Exception Time must be specified` or `Timeband ID must be specified` |
| `400` | Bad request | A value does not resolve: `Invalid Site ID '{siteId}'`, `One or more Product IDs are invalid '{ids}'`, `One or more TimeBand IDs do not exist or are not marked as entry restricted '{ids}'`, `Duplicate Product IDs found in request`, `Invalid day in request. Valid days are: {list}` or `Duplicate Days found in request` |
| `400` | Bad request | The dates are not usable: `Invalid DateTimeFrom or DateTimeTo for time band {timeBandId}`, `DateTimeTo ({to}) must be after DateTimeFrom ({from}) for time band {timeBandId}`, `DateTimeTo time ({hh:mm}) must be after DateTimeFrom time ({hh:mm}) for time band {timeBandId}` or `DateTimeFrom ({from}) cannot be in the past for time band {timeBandId}` |
| `400` | Bad request | The recurrence fields contradict `frequency`: `Invalid Frequency '{value}', must be one of 'Daily', 'Weekly', 'Monthly'`, `RecursEvery cannot be specified for Daily frequency`, `RecursEvery must be greater than zero`, `RecursOn cannot be specified for Daily frequency`, `RecurranceType cannot be specified for Daily frequency`, `At least one day must be specified for Weekly frequency`, `RecursOn cannot be specified for Weekly frequency`, `RecurranceType cannot be specified for Weekly frequency`, `RecurranceType must be specified for Monthly frequency`, `Invalid RecurranceType '{value}', must be one of 'ByDate', 'ByDay'`, `RecursOn must be specified when RecurranceType is 'ByDay'`, `RecursOn must be between 1 and 5`, `Exactly 1 day must be specified when RecurranceType is 'ByDay'`, `Invalid day name '{day}'`, `RecursOn cannot be specified when RecurranceType is 'ByDate'` or `Days cannot be specified when RecurranceType is 'ByDate'` |
| `400` | Bad request | The occurrences generated from your request overlap each other. The body is the full response object, not a message string, with `overlappingExceptions` listing the collisions and `errors` containing `One or more exceptions overlap with each other`. Nothing was written. |
| `400` | Bad request | The write itself failed: `A database error occurred.` |
| `404` | Not found | No exception records exist for the schedule in the path: `No exceptions found for Schedule ID '{scheduleId}'`. This is also what you get for a schedule whose exceptions have all been deleted, so it does not distinguish "never existed" from "emptied". |
| `409` | Conflict | The occurrences you asked for overlap bookings already taken, or exceptions belonging to another schedule, on the same resource products. The body is the full response object, with `existingBookings` and `existingExceptions` naming what collided. Nothing was written; resolve the clashes and retry. |
| `500` | Internal server error | An unexpected server-side failure. Every validation failure returns `400`, `404` or `409`, so a `500` here is not something to work around in the request. |
