# Create one or more booking sheet exceptions

`POST /api/booking-sheets/exception`

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

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

## Overview

Blocks out time on a booking sheet, so no new booking can be taken against the named resource
products during the periods you describe — maintenance, a closure, a school hire, anything that
takes the resource off sale.

One request describes a *recurring pattern*, not a single block. You give a window
(`times[].dateTimeFrom` to `times[].dateTimeTo`), a `frequency` of `Daily`, `Weekly` or `Monthly`,
and the recurrence fields that go with that frequency; the endpoint expands the pattern into
individual exceptions — one per generated date, per entry in `times`, per entry in `productIds` —
and creates them all together under a single **schedule id**, returned as `scheduleId`. Keep that
id: it is how you later amend or remove the whole set, with
[Delete booking sheet exceptions within a date range](delete-booking-sheet-exceptions-within-a-date-range.md).

Nothing is created unless everything can be created. Before creating anything, the endpoint checks the
generated dates against each other, against exceptions that already exist and against bookings
that have already been taken:

- exceptions that overlap **each other** come back as `400`, with the pairs listed in
  `overlappingExceptions`;
- clashes with **existing exceptions or bookings** come back as `409`, listed in
  `existingExceptions` and `existingBookings`, each with the date range, time band and product
  involved — enough to fix the request or to cancel the offending bookings first.

Both bodies are the same shape as the success body, so you can read `errors` in every case.

One case to guard against: if the pattern generates **no dates at all** — every occurrence fell on
an excluded date, or on a weekday not listed in `days` — the call still returns `201 Created`, but
with `exceptionsCreated` of `0` and `No valid dates found for the specified days` in `errors`.
Check `exceptionsCreated` rather than trusting the status code.

Exceptions cannot be created in the past, and each generated block ends one second before the
time of day you give in `dateTimeTo`, so consecutive blocks butt up against each other rather than
overlapping.

## Endpoint

`POST {base_url}/api/booking-sheets/exception`

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

## Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `siteId` | string | Yes | The site whose booking sheet is being blocked out; every product in `productIds` is interpreted within it. Required, and must be an existing site — an unknown value fails with `Invalid Site ID '{siteId}'`, as a `400` rather than a `404`. Omitting it fails with `Site ID must be specified`. |
| `productIds` | array of string | Yes | The resource products to take off sale — a court, a lane, a studio. The whole generated pattern is applied to each one, so a fortnight's closure across three courts is one request, not three. 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 not repeat itself (`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 pattern to particular weekdays. Use full English day names — `monday` through `sunday`, matched case-insensitively; anything else fails with `Invalid day in request. Valid days are: …`, and repeats with `Duplicate Days found in request`. What it means depends on `frequency`: with `Daily`, leaving it out means every day and listing days keeps only those; with `Weekly` at least one day is required (`At least one day must be specified for Weekly frequency`); with `Monthly` and `recurranceType` of `ByDay` exactly one is required (`Exactly 1 day must be specified when RecurranceType is 'ByDay'`); with `Monthly` and `ByDate` it must be left out entirely (`Days cannot be specified when RecurranceType is 'ByDate'`). |
| `times` | array of object | Yes | The time bands to block and the window each pattern runs over. At least one entry is required (`One or more Exception Times must be specified`, or `At least one Exception Time must be specified` for an empty array). Each entry is expanded independently, so listing three time bands over a four-week daily pattern across two products creates 3 × 28 × 2 exceptions in one go. |
| `times[].timeBandId` | string | Yes | The time band on the booking sheet to block — take it from `timeBandId` in [Get timebands](get-timebands.md). The band must exist **and** be marked as entry restricted; if any band in the request fails either test the whole request is rejected with `One or more TimeBand IDs do not exist or are not marked as entry restricted '{ids}'`. Omitting it fails with `Timeband ID must be specified`. |
| `times[].dateTimeFrom` | string (date-time) | Yes | Does double duty: the date the pattern starts, and the time of day every generated block begins at. Required in practice — omitting it fails with `Invalid DateTimeFrom or DateTimeTo for time band {timeBandId}`. It must be in the future (`DateTimeFrom ({value}) cannot be in the past for time band {timeBandId}`), earlier than `dateTimeTo`, and its *time of day* must be earlier than `dateTimeTo`'s, which is what stops a block spanning midnight. For a restriction that crosses midnight, send one entry for each side of it. With `frequency` of `Monthly` and `recurranceType` of `ByDate`, the day number of the **first** entry in `times` is the day of the month the pattern repeats on. |
| `times[].dateTimeTo` | string (date-time) | Yes | The date the pattern stops and the time of day every generated block ends at. Required in practice, on the same grounds as `dateTimeFrom`, and subject to the same two comparisons. Note that each block ends one second *before* this time of day — give `18:00` and the exception runs to `17:59:59` — so a block ending at `18:00` and the next starting at `18:00` do not count as overlapping. |
| `notes` | string | No | Free text stored against every exception this request creates, for whoever looks at the booking sheet later — the reason for the closure, a job number, a contact. It has no effect on availability, and is not used in clash detection. |
| `frequency` | string | Yes | How the pattern repeats: `Daily`, `Weekly` or `Monthly`, matched case-insensitively. Required in practice even though it is not marked as required — omitting it fails with `Invalid Frequency '', must be one of 'Daily', 'Weekly', 'Monthly'`. It decides which of the other recurrence fields are allowed: `Daily` accepts only `days`; `Weekly` requires `days` and accepts `recursEvery`; `Monthly` requires `recurranceType` and accepts `recursEvery`, with `recursOn` and `days` governed by that type. |
| `recursEvery` | integer (int32) | No | How many periods to skip between occurrences: with `Weekly`, `2` means every other week; with `Monthly`, every other month. Defaults to `1` when omitted for those two frequencies, and must be greater than zero (`RecursEvery must be greater than zero`). Meaningless for `Daily` — send it as anything other than `1` and the request fails with `RecursEvery cannot be specified for Daily frequency`. |
| `recurranceType` | string | No | `Monthly` only, and required there (`RecurranceType must be specified for Monthly frequency`). `ByDate` repeats on the same day number each month, taken from `dateTimeFrom` of the first entry in `times` and clamped to the length of shorter months, so the 31st becomes the 30th in April. `ByDay` repeats on the nth named weekday of the month — the second Tuesday, say — set through `recursOn` and `days`. Matched case-insensitively; any other value fails with `Invalid RecurranceType '{value}', must be one of 'ByDate', 'ByDay'`. Sending it with `Daily` or `Weekly` fails with `RecurranceType cannot be specified for {frequency} frequency`. Note the spelling of the field name — one `r`, two `n`s. |
| `recursOn` | integer (int32) | No | Which occurrence of the weekday in the month to use, when `recurranceType` is `ByDay`: `1` for the first, through `5` for the fifth. Required in that case (`RecursOn must be specified when RecurranceType is 'ByDay'`) and limited to that range (`RecursOn must be between 1 and 5`). If a month has no fifth occurrence the pattern moves forward to the next month that does, rather than falling back to the last one. Must be left out everywhere else — with `Daily`, with `Weekly`, and with `recurranceType` of `ByDate` — each of which has its own rejection message. |
| `exclusionDates` | array of string | No | Dates to drop from the generated pattern — bank holidays, a week the works were cancelled. Only the date part is compared, so the time you send is ignored, and a date that the pattern would not have produced anyway is simply not used. If exclusions remove every occurrence, the request returns `201` having created nothing; see the Overview. |
| `additionalDates` | array of string | No | Accepted but not used: dates listed here are silently discarded and no extra exceptions are created for them. To block a date the pattern does not reach, send a second request. |

## Sample request

```bash title="cURL"
curl -X POST '{base_url}/api/booking-sheets/exception' \
  -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"
  ]
}'
```

## Errors

| Status | Meaning | When |
|--------|---------|------|
| `400` | Bad request | A field is missing or malformed: `Site ID must be specified`, `One or more Resource Products must be specified`, `One or more Exception Times must be specified`, `Timeband ID must be specified`, `Invalid DateTimeFrom or DateTimeTo for time band {timeBandId}`. The body is a plain string. |
| `400` | Bad request | A date range is unusable: `DateTimeTo ({to}) must be after DateTimeFrom ({from}) for time band {timeBandId}`, `DateTimeTo time ({to}) must be after DateTimeFrom time ({from}) for time band {timeBandId}`, or `DateTimeFrom ({from}) cannot be in the past for time band {timeBandId}`. |
| `400` | Bad request | The recurrence fields do not match the `frequency`: `Invalid Frequency …`, `RecursEvery cannot be specified for Daily frequency`, `At least one day must be specified for Weekly frequency`, `RecurranceType must be specified for Monthly frequency`, `RecursOn must be between 1 and 5`, and the other combination rules listed against each field above. |
| `400` | Bad request | A list is malformed: `No Product IDs found in request`, `Duplicate Product IDs found in request`, `Invalid day in request. Valid days are: …`, `Duplicate Days found in request`. |
| `400` | Bad request | A referenced record does not exist: `Invalid Site ID '{siteId}'`, `One or more Product IDs are invalid '{ids}'`, or `One or more TimeBand IDs do not exist or are not marked as entry restricted '{ids}'`. These are `400`, not `404`. |
| `400` | Bad request | The exceptions the pattern generated overlap **each other**. The body is the full response object, with the offending pairs in `overlappingExceptions` and `One or more exceptions overlap with each other` in `errors`. Nothing was created. |
| `400` | Bad request | Saving the exceptions failed — `A database error occurred. Please ensure that the ID supplied is unique`. |
| `404` | Not found | This endpoint does not return `404`: every missing site, product and time band is reported as `400`. |
| `409` | Conflict | The generated exceptions clash with exceptions or bookings that already exist. The body is the full response object: `existingExceptions` and `existingBookings` name the date range, time band and product for each clash, and `existingBookings` also carries the `bookingId` you would need to cancel. Nothing was created — resolve the clashes and send the request again. |
| `500` | Internal server error | An unexpected server-side failure, with an empty body. |
