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)
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 thedateTimeFrom–dateTimeTorange in this request.editDateFrommust be null.AllFromHere— only bookings on or aftereditDateFromare deleted and rebuilt; earlier bookings are left alone.editDateFromis 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.
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 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
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
{
"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. |