# Update an existing block booking schedule

`PUT /api/bookings/block-bookings/{scheduleId}`

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

HTML version: [Update an existing block booking schedule](https://support.gladstonesoftware.cloud/reference/bookings/update-an-existing-block-booking-schedule)

## Overview

Replaces the bookings in an existing block booking schedule — a recurring series of reservations held for a single member — with the series this request describes. The schedule keeps its id, but the bookings underneath it are deleted and recreated, so booking ids change on every successful call.

`editType` decides how much of the series is replaced:

- **`All`** — every unpaid booking on the schedule is deleted and the series is rebuilt from the `dateTimeFrom`–`dateTimeTo` range in this request. `editDateFrom` must be null.
- **`AllFromHere`** — only bookings on or after `editDateFrom` are deleted and rebuilt; earlier bookings are left alone. `editDateFrom` is mandatory and cannot precede the first date of the existing series.

Treat the request as a full replacement rather than a patch: the new bookings are built entirely from what you send, so any detail you omit is lost. The exceptions are `reference`, which is copied from the existing bookings when you omit it, `description` (the booking name), which keeps its current value when you omit it and can only be changed with `editType: All`, and `siteId` and `memberId`, which are ignored because both are read from the stored schedule.

`products` determines which dates are actually booked. The recurrence rule (`frequency`, `days`, `recursEvery`, `recurranceType`, `recursOn`) generates candidate occurrences across the date range, and only those occurrences that have a `products` entry for the same calendar date are kept. An occurrence with no matching entry is discarded silently — a forgotten product entry loses a booking rather than raising an error.

Bookings whose invoice has already been paid or collated are never modified. They come back in `retainedBookings`, and their calendar dates are removed from the new series so a duplicate booking is not created on the same day. If the schedule's one-off product invoice has been paid or collated, the whole call is rejected with `422`.

Send `checkOnly=true` first to run validation and clash detection without writing anything. The response then reports the occurrences that would be created in `dates`, with `bookingId` and `salesInvoiceId` set to `0`.

Read the status code together with `errors`. Occurrences that overlap each other return `400`, and clashes with other bookings or resource exceptions return `409`, both with the full response body. Every other failure to proceed returns `200 OK` with `errors` populated, `bookingIds` empty and nothing changed — most commonly `No valid dates found for the specified days`. A `200` with a non-empty `errors` array is not a success.

Creation is not all-or-nothing. If some occurrences fail after the old bookings have already been deleted, the call still returns `200`: successes appear in `bookingIds` and `dates`, failures in `failedBookings`, and `errors` contains `All bookings failed to create` only when none succeeded. Reconcile against `removedBookings` before assuming the series is intact.

## Endpoint

`PUT {base_url}/api/bookings/block-bookings/{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 (uuid) | Yes | The block booking schedule to update, as returned in `scheduleId` when the schedule was created. An id with no matching schedule returns `404`. |

## Query parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `checkOnly` | boolean | No | Set to true to validate the request and run clash detection without changing anything — no bookings are deleted or created. The occurrences that would be created are returned in `dates` with `bookingId` and `salesInvoiceId` as `0`. Some checks are skipped in this mode: `activityGroupId` is not required or verified, and the per-entry `products` checks (quantity, price, tax code, time band, duplicates) do not run, so a request that passes with `checkOnly=true` can still fail without it. Defaults to false. |

## Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `siteId` | string | No | Ignored. Any value you send is replaced with the schedule's existing site, so an update cannot move a series to another site. |
| `memberId` | integer (int32) | No | Ignored. Any value you send is replaced with the schedule's existing member, so an update cannot reassign a series to another member. |
| `editType` | string | Yes | How much of the series to replace. `All` rebuilds the whole series from this request; `AllFromHere` rebuilds only occurrences on or after `editDateFrom`. Case-insensitive. Omitting it is a `400`, as is any value outside `All`, `AllFromHere` and `ThisDateOnly`; `ThisDateOnly` is accepted by the parser but rejected with `422` because it is not implemented. |
| `editDateFrom` | string (date-time) | No | The first occurrence the edit applies to. Required when `editType` is `AllFromHere` and must be null when `editType` is `All` — either mismatch is a `400`. It cannot be earlier than the first date of the existing series. Occurrences before this date keep their current bookings untouched. Sent as UTC and converted using its own offset, unlike the other date fields. |
| `products` | array of object | Yes | The priced lines to attach to each booking — one or more entries per booking date, and the source of truth for which dates are booked. An occurrence generated by the recurrence rule with no entry for its calendar date is discarded without an error; entries whose date matches no occurrence are ignored. Omitting the field or sending an empty array is a `400`, for both edit types and even when `checkOnly=true`. |
| `products[].date` | string (date-time) | Yes | The booking date this line belongs to. Only the calendar date is compared against the occurrence's start date — the time of day is ignored. Sent as UTC and converted using `dateTimeFrom`'s offset. A missing or zero value is a `400`. |
| `products[].productId` | string | Yes | The product to charge for on this date. Must be an existing standard product; unknown ids come back in a `400` naming them. The same product may appear on many dates, but two entries sharing a date and product id are rejected because they would raise duplicate invoice lines on one booking. |
| `products[].quantity` | integer (int32) | Yes | Units of the product to charge on this date. Must be greater than zero. |
| `products[].price` | number (double) | Yes | Unit price to charge, overriding the product's configured price. Must be zero or greater; negative values are a `400`. |
| `products[].taxCodeId` | string | Yes | Tax code used to look up the tax rate applied to this line. Must be supplied and must exist, or the request fails with a `400` naming the invalid ids. |
| `products[].timeBandId` | string | Yes | The [time band](../../guides/glossary.md) this line is booked under. Must be supplied and must be an existing time band id, or the request fails with a `400` naming the invalid ids. |
| `activityGroupId` | string | Yes | The activity group the schedule's underlying activity belongs to. Must be an existing activity group id. Required for a real update — omitting it is a `400` — but not required or checked when `checkOnly=true`. Sending a different value from the stored one moves the activity into that group. |
| `frequency` | string | Yes | The rule used to generate candidate occurrences between `dateTimeFrom` and `dateTimeTo`. One of `Daily`, `Weekly` or `Monthly`, case-insensitive; anything else is a `400` listing the valid values. It also decides which of `recursEvery`, `recurranceType`, `recursOn` and `days` may be sent. |
| `recursEvery` | integer (int32) | No | The interval between occurrences: every N weeks for `Weekly`, every N months for `Monthly`. Defaults to 1 when omitted, and must be greater than zero. Must not be sent when `frequency` is `Daily` — it is rejected with a `400` rather than ignored. |
| `recurranceType` | string | No | How a monthly series picks its day. `ByDate` repeats on the day-of-month taken from `dateTimeFrom`; `ByDay` repeats on the weekdays in `days` at the ordinal position given by `recursOn`. Case-insensitive. Mandatory when `frequency` is `Monthly`, and must be omitted for `Daily` and `Weekly`. |
| `recursOn` | integer (int32) | No | Which occurrence of the weekday within the month, from 1 to 5. Required when `recurranceType` is `ByDay`; must be omitted in every other case, including `ByDate`, `Daily` and `Weekly`. |
| `paymentMethodId` | string | No | The payment method the bookings are billed against. Omit it and the site's default host payment method is used instead. Either way the effective method must resolve to an invoicing account and the member must have one configured: an unknown id is a `400`, a member without a matching invoicing account is a `400`, and a valid method that is not an invoicing account is a `422`. Block bookings cannot be paid any other way. |
| `resourceProductIds` | array of string | Yes | The resource products the series occupies — the bookable items being reserved, as distinct from the priced lines in `products`. At least one is required; duplicates are rejected and every id must exist. These ids drive clash detection: any other booking or any exception on the same resource product overlapping a new occurrence is reported in `existingBookings` or `existingExceptions` and the call fails with `409`. |
| `days` | array of string | No | Weekday names restricting which days an occurrence may fall on — `sunday` through `saturday`, case-insensitive, no duplicates. Omit it or send an empty array to allow every day. At least one entry is required when `recurranceType` is `ByDay`, and the field must be omitted when `recurranceType` is `ByDate`. |
| `dateTimeFrom` | string (date-time) | Yes | Start of the series, and the time of day at which every occurrence starts. Sent as UTC and converted to the centre's local time. A missing or zero value is a `400`. |
| `dateTimeTo` | string (date-time) | Yes | End of the series, and the time of day at which every occurrence ends. Must be later than `dateTimeFrom` both as an instant and as a time of day, so an occurrence cannot run past midnight. Converted using `dateTimeFrom`'s UTC offset so occurrences keep the same wall-clock times either side of a daylight-saving change. |
| `exclusionDates` | array of string | No | Dates within the `dateTimeFrom`–`dateTimeTo` range to skip. Only the calendar date is used. Every entry must fall inside the range and no date may repeat, or the request fails with `400`. Echoed back in `excludedDates`. |
| `notes` | string | No | Free-text note copied onto every booking this call creates. |
| `reference` | string | No | Your own reference, copied onto every booking this call creates. Maximum 255 characters. Omit it, or send null, and the reference is copied from the schedule's existing bookings so the value survives the update; send an empty string to replace it with a blank. |
| `activityId` | string | No | Leave unset. Before the update is applied this is replaced with the activity already attached to the schedule, so a value you send cannot change which activity is used — and because supplied ids are checked for uniqueness first, sending the schedule's own activity id fails with `400 ActivityId '…' already exists`. |
| `description` | string | No | The booking name, shown against every occurrence in the series. Omit it, or send null, an empty string or whitespace, and the current name is kept, whatever `editType` you send. To rename the series, send a different value with `editType: All`: the new name applies to the whole series, including bookings returned in `retainedBookings`. With any other `editType` the value must match the current name exactly (the comparison is case-sensitive), or the request is rejected with `400` and nothing is changed; send the current name or omit the field. Maximum 25 characters: a longer value is rejected with `400` before any booking is deleted. |
| `invoiceGroupingPeriod` | integer (int32) | Yes | Sets the underlying activity's invoice grouping period indicator, which governs how the bookings' invoices are grouped together. Mandatory — omitting it fails with `Invoice Grouping Period must be specified`. The accepted range is 0 to 255. |
| `invoiceItemisation` | boolean | Yes | Whether the invoices raised for these bookings itemise each product line. Mandatory — omitting it fails with `Invoice Itemisation must be specified`. Stored on the activity as 1 for true and 0 for false. |
| `participantNumber` | integer (int32) | No | Theoretical participant capacity, recorded for reporting only — it is not enforced against the people actually booked in. The single value is stored on the activity as both the minimum and maximum bookee count. Omit it to leave the stored value unchanged. Must be between 0 and 32767. |

## Sample request

```bash title="cURL"
curl -X PUT '{base_url}/api/bookings/block-bookings/{scheduleId}' \
  -H 'Authorization: Bearer {accessToken}' \
  -H 'X-Use-SSO: true' \
  -H 'Content-Type: application/json' \
  -d '{
  "siteId": "string",
  "memberId": 12345,
  "editType": "string",
  "editDateFrom": "2026-08-14T19:00:00Z",
  "products": [
    {
      "date": "2026-08-14T19:00:00Z",
      "productId": "string",
      "quantity": 1,
      "price": 10.5,
      "taxCodeId": "string",
      "timeBandId": "string"
    }
  ],
  "activityGroupId": "string",
  "frequency": "string",
  "recursEvery": 1,
  "recurranceType": "string",
  "recursOn": 1,
  "paymentMethodId": "string",
  "resourceProductIds": [
    "string"
  ],
  "days": [
    "string"
  ],
  "dateTimeFrom": "2026-08-14T19:00:00Z",
  "dateTimeTo": "2026-08-14T19:00:00Z",
  "exclusionDates": [
    "2026-08-14T19:00:00Z"
  ],
  "notes": "string",
  "reference": "string",
  "activityId": "string",
  "description": "string",
  "invoiceGroupingPeriod": 1,
  "invoiceItemisation": true,
  "participantNumber": 1
}'
```

## Response fields

| Field | Type | Description |
|-------|------|-------------|
| `bookingIds` | array of integer | Ids of the bookings this call created, in the same order as `dates`. Empty when `checkOnly=true`, when `errors` is non-empty, or when every occurrence failed. Ids are never reused from the bookings that were replaced — those are listed in `removedBookings`. |
| `removedBookings` | array of object | The bookings deleted to make room for the new series. With `editType: AllFromHere` these are the schedule's bookings on or after `editDateFrom`; with `All` they are all of the schedule's unpaid bookings. Always empty when `checkOnly=true`. |
| `removedBookings[].bookingId` | integer (int32) | Id of the deleted booking. Use it to retire the record on your side and match it against the replacements in `bookingIds`. |
| `removedBookings[].salesInvoiceId` | integer (int32) | Id of the sales invoice that was attached to the deleted booking. |
| `retainedBookings` | array of object | Bookings deliberately left untouched because their invoice is already paid or collated. Their calendar dates are removed from the new series, so no replacement booking is created on those days and they are absent from `dates`. |
| `retainedBookings[].bookingId` | integer (int32) | Id of the retained booking. It keeps every detail it already had — this update did not modify it. |
| `retainedBookings[].salesInvoiceId` | integer (int32) | Sales invoice on the retained booking, or `0` if no member line on it carries one. |
| `retainedBookings[].startDate` | string (date-time) | Start of the retained booking as already stored. Unlike `dates[].startDate`, this is the centre's local (wall-clock) time and is not converted to UTC. |
| `retainedBookings[].endDate` | string (date-time) | End of the retained booking as already stored, in the centre's local (wall-clock) time. |
| `retainedBookings[].reason` | string | Why the booking was left alone. Either `Invoice has been paid` or `Invoice has been collated`. |
| `dates` | array of object | The bookings this call created, with their times. When `checkOnly=true` it instead lists the occurrences that would be created. |
| `dates[].bookingId` | integer (int32) | Id of the created booking — the same value as the matching entry in `bookingIds`. Always `0` when `checkOnly=true`, because no booking was created. |
| `dates[].salesInvoiceId` | integer (int32) | Sales invoice raised for the created booking. Always `0` when `checkOnly=true`. |
| `dates[].startDate` | string (date-time) | Start of the created occurrence, converted to UTC. |
| `dates[].endDate` | string (date-time) | End of the created occurrence, converted to UTC. |
| `excludedDates` | array of string | The `exclusionDates` you sent, echoed back as calendar dates with no time component. Empty when you sent none. |
| `scheduleId` | string (uuid) | The schedule that was updated — always the id you sent in the path. Updating never issues a new schedule id. |
| `selectedDates` | array of object | Every occurrence the recurrence rule produced for this request, before occurrences earlier than `editDateFrom` and dates held by paid or collated bookings were filtered out. Compare it with `dates` to see which candidates were actually booked. |
| `selectedDates[].startDate` | string (date-time) | Start of the generated occurrence, converted to UTC. |
| `selectedDates[].endDate` | string (date-time) | End of the generated occurrence, converted to UTC. |
| `errors` | array of string | Why the update did not proceed as asked. Known values are `No valid dates found for the specified days`, `One or more bookings overlap with each other`, `One or more bookings overlap with existing exceptions for the same product(s)`, `One or more bookings overlap with existing bookings for the same product(s)` and `All bookings failed to create`. Every value except the last means nothing was written; the last means the old bookings were deleted and no replacement could be created. |
| `overlappingDates` | array of object | Occurrences in the new series that overlap another occurrence in the same series. A non-empty list means the call returned `400` and nothing was changed. |
| `overlappingDates[].dateTimeFrom` | string (date-time) | Start of the overlapping occurrence, converted to UTC. |
| `overlappingDates[].dateTimeTo` | string (date-time) | End of the overlapping occurrence, converted to UTC. |
| `overlappingDates[].productId` | string | Never returned. This field is absent from `overlappingDates` entries, even though the same object shape carries it under `existingBookings` and `existingExceptions`. |
| `existingBookings` | array of object | Bookings that already occupy one of your `resourceProductIds` during a new occurrence. The schedule being updated is excluded, so its own bookings never appear here. A non-empty list means the call returned `409` and nothing was changed. |
| `existingBookings[].bookingId` | integer (int32) | Id of the clashing booking. |
| `existingBookings[].recurringScheduleId` | string (uuid) | The block booking schedule the clashing booking belongs to, or null when it is a standalone booking. |
| `existingBookings[].dateTimeFrom` | string (date-time) | Start of the clashing booking, converted to UTC. |
| `existingBookings[].dateTimeTo` | string (date-time) | End of the clashing booking, converted to UTC. |
| `existingBookings[].productId` | string | The resource product both bookings need — the reason they clash. |
| `existingExceptions` | array of object | Exception periods held against one of your `resourceProductIds` that overlap a new occurrence. An exception is a dated period recorded against a resource product; a booking cannot be placed over one. A non-empty list means the call returned `409` and nothing was changed. |
| `existingExceptions[].dateTimeFrom` | string (date-time) | Start of the exception period, converted to UTC. |
| `existingExceptions[].dateTimeTo` | string (date-time) | End of the exception period, converted to UTC. |
| `existingExceptions[].productId` | string | The resource product the exception is held against. |
| `failedBookings` | array of object | Occurrences that could not be created after the old bookings had already been deleted. A non-empty list on a `200` response means the update was applied only in part — the schedule now holds fewer bookings than you asked for. |
| `failedBookings[].startDate` | string (date-time) | Start of the occurrence that could not be created. This is the centre's local (wall-clock) time and, unlike `dates[].startDate`, is not converted to UTC. |
| `failedBookings[].endDate` | string (date-time) | End of the occurrence that could not be created, in the centre's local (wall-clock) time. |
| `failedBookings[].error` | string | The underlying exception message for that occurrence, passed through as-is. Treat it as a diagnostic string rather than a stable code. |

## Sample response

```json title="200 OK"
{
  "bookingIds": [
    12345
  ],
  "removedBookings": [
    {
      "bookingId": 12345,
      "salesInvoiceId": 12345
    }
  ],
  "retainedBookings": [
    {
      "bookingId": 12345,
      "salesInvoiceId": 12345,
      "startDate": "2026-08-14T19:00:00Z",
      "endDate": "2026-08-14T19:00:00Z",
      "reason": "string"
    }
  ],
  "dates": [
    {
      "bookingId": 12345,
      "salesInvoiceId": 12345,
      "startDate": "2026-08-14T19:00:00Z",
      "endDate": "2026-08-14T19:00:00Z"
    }
  ],
  "excludedDates": [
    "string"
  ],
  "scheduleId": "b7f3c1e2-9a44-4f2b-8c17-0d5e9a1b6c33",
  "selectedDates": [
    {
      "startDate": "2026-08-14T19:00:00Z",
      "endDate": "2026-08-14T19:00:00Z"
    }
  ],
  "errors": [
    "string"
  ],
  "overlappingDates": [
    {
      "dateTimeFrom": "2026-08-14T19:00:00Z",
      "dateTimeTo": "2026-08-14T19:00:00Z",
      "productId": "string"
    }
  ],
  "existingBookings": [
    {
      "bookingId": 12345,
      "recurringScheduleId": "b7f3c1e2-9a44-4f2b-8c17-0d5e9a1b6c33",
      "dateTimeFrom": "2026-08-14T19:00:00Z",
      "dateTimeTo": "2026-08-14T19:00:00Z",
      "productId": "string"
    }
  ],
  "existingExceptions": [
    {
      "dateTimeFrom": "2026-08-14T19:00:00Z",
      "dateTimeTo": "2026-08-14T19:00:00Z",
      "productId": "string"
    }
  ],
  "failedBookings": [
    {
      "startDate": "2026-08-14T19:00:00Z",
      "endDate": "2026-08-14T19:00:00Z",
      "error": "string"
    }
  ]
}
```

## Errors

| Status | Meaning | When |
|--------|---------|------|
| `400` | Bad request | `editType` is missing (`Edit Type must be specified`) or is not one of `All`, `AllFromHere` or `ThisDateOnly`. |
| `400` | Bad request | `editDateFrom` was sent with `editType: All`, or omitted with `editType: AllFromHere`. |
| `400` | Bad request | `editDateFrom` is earlier than the first date of the existing series. |
| `400` | Bad request | `products` is missing or empty (`Products must be specified`). Raised for both edit types, and even when `checkOnly=true`. |
| `400` | Bad request | `activityGroupId` is missing on a real update (`Activity Group ID must be specified`). Not raised when `checkOnly=true`. |
| `400` | Bad request | A `products` entry is missing `date`, `taxCodeId` or `timeBandId`, has a `quantity` of zero or less, or a negative `price`; or two entries share the same date and `productId`. |
| `400` | Bad request | An id does not exist. The message names which: site, activity group, payment method, member, product, resource product, tax code or time band. |
| `400` | Bad request | `resourceProductIds` is empty or contains duplicate ids. |
| `400` | Bad request | The recurrence combination is not allowed — for example `recursEvery` or `recursOn` sent with `frequency: Daily`, `recurranceType` missing for `Monthly`, `recursOn` outside 1–5, or `days` sent with `recurranceType: ByDate`. |
| `400` | Bad request | `dateTimeFrom` or `dateTimeTo` is missing or zero, `dateTimeTo` is not after `dateTimeFrom`, or its time of day is not after `dateTimeFrom`'s. |
| `400` | Bad request | An entry in `exclusionDates` falls outside the `dateTimeFrom`–`dateTimeTo` range, or the same date appears twice. |
| `400` | Bad request | `activityId` was supplied and already exists, or exceeds the maximum activity id length. |
| `400` | Bad request | The member has no invoicing account configured for the effective `paymentMethodId`. |
| `400` | Bad request | `description` is longer than 25 characters (`Booking name cannot be greater than 25 characters`). Nothing is deleted or changed. |
| `400` | Bad request | `description` differs from the series' current name and `editType` is not `All` (`The booking name can only be changed when EditType is 'All'`). A booking name can only be changed for the whole series. Nothing is deleted or changed. |
| `400` | Bad request | Occurrences in the new series overlap each other. The body is the full response object with `overlappingDates` populated; nothing was changed. |
| `400` | Bad request | The update could not be saved: `A database error occurred. Please ensure that the ID supplied is unique`. |
| `404` | Not found | No schedule exists with the `scheduleId` in the path (`Schedule with ID '…' not found`). |
| `409` | Conflict | New occurrences clash with bookings already held on the same resource products, or with exception periods on them. The body is the full response object with `existingBookings` and `existingExceptions` populated; nothing was changed. |
| `422` | Unprocessable entity | `editType` is `ThisDateOnly` — accepted by the parser but not implemented. |
| `422` | Unprocessable entity | The schedule's one-off product invoice has already been paid or collated, so the block booking cannot be modified at all. |
| `422` | Unprocessable entity | The effective `paymentMethodId` is not an invoicing account. Block bookings must be paid via one. |
| `500` | Internal server error | An unexpected server-side failure. The response has no body — retrying is safe only after checking the schedule's current state, because bookings may already have been deleted. |
