# Bookings

A booking represents a guest or resident's stay in one of your units, from the moment it is requested through to
checkout, cancelation, or termination. A booking carries the contractual stay window, the people on it, its
commercial terms (rate, discounts, optional services), its payment and vetting state, and a record of what actually
happened physically (when the resident arrived and departed).

Bookings are the anchor point for most operational activity in the Lavanda platform. A unit's availability, a guest's
messages, an invoice's line items, and a checklist's tasks are all reached through, or scoped to, a booking.

## Key Concepts

### Booking status

A booking moves through a status lifecycle as it is requested, confirmed, occupied, and closed out:

`INQUIRY`, `REQUESTED`, `RESERVED`, `TENTATIVE`, `TENTATIVE_EXPIRED`, `AWAITING_PAYMENT`, `CONFIRMED`, `CHECKED_IN`,
`CHECKED_OUT`, `DECLINED`, `EXPIRED`, `CANCELED`, `TERMINATED`, `CLOSED`.

> **Warning:** `CHECKED_IN` and `CHECKED_OUT` are deprecated. They are not stored lifecycle statuses: a confirmed
> booking reports `CHECKED_IN` once an arrival is recorded **or** its check-in date has passed, and `CHECKED_OUT` once
> a departure is recorded **or** its check-out date has passed. That means a resident who never arrived can still
> report `CHECKED_IN`, and one who has not left can report `CHECKED_OUT`. Read
> [`occupancyStatus`](#occupancy-status) to find out whether the resident is actually in residence. Once these two
> values are removed, those bookings will report `CONFIRMED`. We will announce the removal date before it happens.

A **tentative** booking is a soft hold with a time-to-live (its `bookingTtl`); it must be confirmed before that TTL
expires or it is automatically expired. See [Tentative bookings](#tentative-bookings) below.

### Three layers of dates: contract, scheduled, and actual

A booking tracks three distinct kinds of dates, and the API keeps them intentionally separate:

| Layer | Fields | What it means |
|-------|--------|----------------|
| **Contract** | `checkIn`, `checkOut` (aliased as `contractStartDate`, `contractEndDate`) | The legal tenancy window agreed with the guest. This is the booking's binding stay window. |
| **Scheduled** | `scheduledMoveInAt`, `scheduledMoveOutAt` | The planned physical move date and time. Defaults to the contract window (at the property's check-in/check-out time) but can be overridden, for example when a guest asks to move in a few hours later than their contract start. |
| **Actual** | `arrivedAt`, `departedAt` | When the resident actually, physically arrived and departed, recorded by `markArrived` and `markDeparted`. Null until recorded. |

Keeping these separate means a late arrival, an early departure, or a rescheduled move-in never overwrites the
contract dates you agreed with the guest.

### Occupancy status

`occupancyStatus` is the recorded physical-presence state of the booking: whether the resident is currently checked
in, checked out, marked as a no-show, or temporarily away.

> **Note:** `occupancyStatus` is only ever set by an explicit action (marking arrived, marking departed, or flagging
> a no-show). It is **never inferred** from the contract or scheduled dates. A booking whose check-in date has
> passed but whose resident was never marked as arrived still reports `occupancyStatus: null`.

| Value | Description |
|-------|-------------|
| `CHECKED_IN` | The resident has physically arrived and not yet departed |
| `CHECKED_OUT` | The resident has physically departed |
| `NO_SHOW` | The resident did not arrive within the expected check-in window |
| `AWAY` | The resident is temporarily away from the unit |
| `OVERSTAY` | The resident did not depart within the expected check-out window |

`checkedInStatus` is a related, coarser field covering the same ground (`NOT_CHECKED_IN`, `CHECKED_IN`,
`CHECKED_OUT`, `NO_SHOW`, `ABSCONDED`). Prefer `occupancyStatus` for new integrations; `checkedInStatus` is kept for
backward compatibility.

`canCheckIn` and `canCheckOut` tell you, for a given booking, whether it is currently appropriate to mark it arrived
or departed (confirmed, not yet arrived or departed, and the property-local date is due), which is useful for
deciding whether to show a check-in or check-out action in your own UI. Gate check-in on `canCheckIn`, not on
`scheduledMoveInAt`: the scheduled move-in can be set earlier than the contract check-in date. The `markArrived` /
`markDeparted` mutations do not enforce these conditions, except that `markArrived` refuses an arrival before the
contract check-in date; see [Recording arrival and departure](#recording-arrival-and-departure) below.

### Tentative bookings

A newly created booking can be **tentative**: a soft hold on a unit that must be confirmed before its time-to-live
(`bookingTtl`) expires. Use `confirmTentativeBooking` to turn it into a `CONFIRMED` booking, or
`expireTentativeBooking` to release the hold early. If neither happens before the TTL elapses, the booking is
automatically transitioned to `TENTATIVE_EXPIRED`.

### Relocation, moving, and splitting

`relocationPolicy` is the operator's stated intent for whether a booking may be moved off its current unit
(`UNRESTRICTED` by default, or `LOCKED` to pin it in place, for example after a guest was relocated for accessibility
reasons). `relocatable` reflects whether the booking can currently be moved, combining that policy with structural
eligibility checks.

- `moveBooking` relocates all or part of a booking's stay from one unit to another.
- `splitBooking` divides a booking's stay into multiple segments, each pinned to its own unit and date range, for
  example when a guest needs to change rooms partway through their stay.

Both operations run asynchronously; see [Moving and splitting a booking](#moving-and-splitting-a-booking).

### Canceling and terminating a booking

There are two ways a confirmed booking stops early:

- **Direct cancellation** (`cancelBooking`) cancels a booking immediately. It is scoped to direct bookings and is not
  a reviewable, multi-party workflow.
- **Cancelation and termination requests** are an explicit, reviewable alternative: one caller opens a request, and
  another (or the same caller, if permitted) approves, declines, or withdraws it. A cancelation request applies to a
  booking whose stay has not started; a termination request ends a booking whose stay has already started. Both are
  per-workspace opt-in features with their own eligibility rules, permissions, and webhook events.
  See [Cancelation Requests](/bookings/cancelation-requests/) for the pre-stay workflow and
  [Termination Requests](/bookings/termination-requests/) for the in-stay workflow.

### Group bookings

Related bookings, for example multiple rooms booked together for the same stay, can be grouped under a shared
context reachable via `Booking.context`. `updateGroupBookingName` renames the group.

### Contacts and roles

A booking's `contacts` connection lists every guest, guarantor, and booking agent attached to it, each with a `role`
(for example lead guest, guest, guarantor, or booking agent) and the underlying CRM contact. `addContactsToBooking`
and `removeContactFromBooking` manage this list; `changeBookingLeadGuest` reassigns which contact is the lead guest.
See [Contacts](/crm/contacts/) for the CRM contact model itself.

### Discounts and optional services

A booking can carry coupon-backed discounts (`discounts`, applied and removed via `applyDiscount`, `removeDiscount`,
and `redeemCoupon`) and optional services (`services`, managed via `addOptionalServices`, `updateOptionalServices`,
and `removeOptionalServices`), both of which feed into the booking's commercial value alongside its accommodation
charge.

### Checklists

Every booking can carry a checklist of operational tasks (for example vetting or key handover steps), reachable via
`Booking.checklist`. See [Checklists](#checklists-1) below.

## GraphQL API

### The `Booking` type

The fields below are grouped by theme. Every field is nullable unless marked `!`.

#### Identity and status

| Field | Type | Description |
|-------|------|-------------|
| `id` | `String!` | The booking's unique identifier. Kept as `String` rather than `ID` for backward compatibility with existing clients. |
| `status` | `BookingStatusType` | The booking's status (see [Booking status](#booking-status)). The `CHECKED_IN` and `CHECKED_OUT` values are deprecated; use `occupancyStatus` instead. |
| `confirmationCode` | `String` | The confirmation code of the booking |
| `externalBookingId` | `String` | The booking's ID from an external source, when the booking originated on another platform |
| `platform` | `String` | The platform the booking originated on |
| `createdAt` | `ISO8601DateTime!` | When the booking was created |
| `updatedAt` | `ISO8601DateTime!` | When the booking was last updated |
| `editable` | `Boolean` | Whether the booking is currently editable |
| `cancelable` | `Boolean` | Whether the booking is cancelable via direct cancellation |
| `approvable` | `Boolean` | Whether the booking can be approved by the host, for enquiry-based bookings awaiting host acceptance |

#### Dates and occupancy

| Field | Type | Description |
|-------|------|-------------|
| `checkIn` / `checkOut` | `ISO8601Date` | The contractual check-in and check-out dates |
| `contractStartDate` / `contractEndDate` | `ISO8601Date` | Aliases of `checkIn` / `checkOut` |
| `scheduledMoveInAt` / `scheduledMoveOutAt` | `ISO8601DateTime` | The planned physical move date and time |
| `arrivedAt` / `departedAt` | `ISO8601DateTime` | When the resident actually arrived and departed |
| `occupancyStatus` | `OccupancyStatusType` | The recorded physical-presence state (see [Occupancy status](#occupancy-status)) |
| `checkedInStatus` | `CheckedInStatusType` | A coarser, legacy presence state |
| `canCheckIn` / `canCheckOut` | `Boolean!` | Whether the booking is eligible to be marked arrived / departed today |
| `plannedArrival` / `plannedDeparture` | `String` | The guest's stated planned arrival and departure time |
| `nightsCount` | `Int` | The number of nights of the booking |

#### Guests and contacts

| Field | Type | Description |
|-------|------|-------------|
| `guestsCount` | `Int` | The number of guests on the booking |
| `contacts` | `BookingContact` connection | The guest, guarantor, and booking-agent contacts on the booking |
| `bookingAgent` | `CrmContact` | The booking agent associated with the booking, if any |
| `guarantorInformations` | `[BookingGuarantorInformation!]!` | Typed booking-to-guarantor relationships |
| `leadGuestChangeAbility` | `LeadGuestChangeAbility!` | Whether the current caller can change the lead guest, and why not if disabled |

#### Contract, relocation, and structure

| Field | Type | Description |
|-------|------|-------------|
| `contractType` | `ContractTypeEnum` | The type of contract (short-term licence, AST, or other) |
| `stayPurpose` | `StayPurposeEnum` | The purpose of the stay (holiday, student, or residential) |
| `relocationPolicy` | `BookingRelocationPolicy!` | Operator intent on whether the booking may be relocated |
| `relocatable` | `Boolean` | Whether the booking can currently be relocated |
| `accommodations` | `[Accommodation!]` | The unit allocation history behind the booking, including the current one |
| `product` | `Product` | The product the booking was made against |
| `context` | `BookingContext` | The group of related bookings this booking belongs to, if any |
| `contracts` | `[Contract!]` | The booking's e-signed contracts, with their envelope and e-signing account details |

#### Commercial and payment

| Field | Type | Description |
|-------|------|-------------|
| `currentContractValue` | `ContractValue` | The booking's current commercial value |
| `customBillingCycle` | `Boolean!` | Whether the booking uses a custom billing cycle (instalment plan) |
| `instalmentDriven` | `Boolean!` | Whether the billing cycle carries authoritative per-instalment amounts |
| `billingPeriods` | `BillingPeriod` connection | The booking's billing periods |
| `discounts` | `BookingDiscount` connection | The coupon-backed discounts applied to the booking |
| `services` | `BookingOptionalService` connection | The optional services applied to the booking |
| `paymentStatus` | `PaymentStatusType` | The booking's payment status |
| `downPaymentStatus` | `DownPaymentStatusType` | The booking's down payment status |
| `sepaStatus` | `SepaStatusType` | The booking's SEPA mandate status |
| `vetting` | `Vetting` | The booking's vetting state |

#### Cancelation and termination

| Field | Type | Description |
|-------|------|-------------|
| `cancellationReason` | `String` | The reason recorded if the booking was canceled |
| `cancelationReasons` | `[BookingCancelationReasonEnum!]` | Cancelation reasons applicable given the booking's current timing |
| `cancelationPolicy` | `BookingCancellationPolicy` | The cancelation policy that applies to the booking |
| `cancelationRequests` | connection | All cancelation requests opened against the booking. See [Cancelation Requests](/bookings/cancelation-requests/). |
| `terminationRequests` | connection | All termination requests opened against the booking. See [Termination Requests](/bookings/termination-requests/). |

#### Operational

| Field | Type | Description |
|-------|------|-------------|
| `bookingTtl` | `BookingTtl` | The time-to-live of a tentative booking |
| `checklist` | `Checklist` | The booking's checklist. See [Checklists](#checklists-1). |
| `conversation` | `Conversation` | The booking's guest conversation |
| `messagingMode` | `MessagingModeEnum` | How guest messages are routed for this booking |
| `channelConnectionStatus` | `ChannelConnectionStatusEnum!` | Whether replies can still be relayed to the guest through the booking's channel connection |
| `enabledConversation` | `Boolean` | Whether messaging is enabled for the booking |
| `messageThreadEmailAddress` | `String` | The email address used for the booking's message thread |

### Key enums

**`BookingStatusType`**: see [Booking status](#booking-status) for the full value list. `CHECKED_IN` and
`CHECKED_OUT` are deprecated in favor of `occupancyStatus`.

**`OccupancyStatusType`** — see [Occupancy status](#occupancy-status) for the full value list and descriptions.

**`ContractTypeEnum`**

| Value | Description |
|-------|-------------|
| `SHORT_TERM_LICENCE` | Short-term licence, the default for holiday lets |
| `AST` | Assured Shorthold Tenancy (UK residential) |
| `OTHER` | Other contractual arrangement |

**`StayPurposeEnum`**

| Value | Description |
|-------|-------------|
| `HOLIDAY` | Short-term holiday let |
| `STUDENT` | Student accommodation |
| `RESIDENTIAL` | Long-term residential let |

**`BookingRelocationPolicy`**

| Value | Description |
|-------|-------------|
| `UNRESTRICTED` | Relocatable, subject to the usual structural checks. The default. |
| `LOCKED` | Pinned to its current unit; cannot be relocated |

**`DownPaymentStatusType`**: `COMPLETED`, `PENDING`, `REFUNDED`

**`PaymentStatusType`**: `DUE`, `IN_PROCESS`, `OVERDUE`, `SUCCESS`

**`SepaStatusType`**: `COMPLETED`, `FAILED`, `PENDING`

> **Note:** `search_bookings` (see [MCP Tools](#mcp-tools)) filters on a `PaymentStatusType` with a broader set of
> values (including `CANCELED`, `DISPUTED`, `EXPIRED`, `FAILED`, and `REVERSED`), served by a separate search index.
> The `PaymentStatusType` returned directly on `Booking.paymentStatus` uses the smaller set above.

## Querying bookings

### Get a booking by ID

```graphql
query GetBooking($bookingId: ID!) {
  bookings {
    booking(bookingId: $bookingId) {
      id
      status
      checkIn
      checkOut
      occupancyStatus
      arrivedAt
      departedAt
      confirmationCode
      contacts {
        nodes {
          role
          contact {
            id
            firstName
            lastName
          }
        }
      }
    }
  }
}
```

```json
{
  "bookingId": "booking-id"
}
```

### List and filter bookings

```graphql
query SearchBookings($filters: BookingsFiltersInput, $first: Int) {
  bookings {
    bookings(filters: $filters, first: $first) {
      totalCount
      nodes {
        id
        status
        checkIn
        checkOut
        occupancyStatus
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}
```

```json
{
  "filters": {
    "status": { "eq": "CONFIRMED" },
    "checkIn": { "gte": "2026-09-01T00:00:00Z" }
  },
  "first": 25
}
```

This follows the standard [pagination pattern](/concepts/api-paradigms/connections/) used across the
API. For the complete `BookingsFiltersInput` and `BookingsSortInput` field reference, see
[search_bookings](/mcp/tools/bookings/search_bookings/), which documents every supported filter
(status, dates, contacts, vetting, payment, contract, value, and more).

### Look up bookings by ID in bulk

For a lightweight bulk lookup by a known set of IDs, without filters or sorting:

```graphql
query BookingsByIds($ids: [String!]) {
  bookings {
    search(q: { ids: $ids }) {
      id
      status
    }
  }
}
```

```json
{
  "ids": ["booking-id-1", "booking-id-2"]
}
```

### Download a signed contract from the e-signing provider

Each contract carries its DocuSign envelope ID (`externalId`) and, under `esigningEmbed`, the coordinates of the
workspace's DocuSign account. A client serving several workspaces reads them per booking instead of keeping its own
workspace-to-account mapping.

```graphql
query BookingContracts($bookingId: ID!) {
  bookings {
    booking(bookingId: $bookingId) {
      contracts {
        externalId
        status
        esigningEmbed {
          environment
          accountId
          apiBaseUri
        }
      }
    }
  }
}
```

To fetch the signed documents of a `COMPLETED` contract, call DocuSign with your own integration, granted access
to the account by its owner:
`GET {apiBaseUri}/restapi/v2.1/accounts/{accountId}/envelopes/{externalId}/documents/combined`. The API exposes
identifiers only, never Lavanda's DocuSign credentials. `esigningEmbed` is null when the workspace has no
e-signing account, and `accountId` or `apiBaseUri` is null while it has none recorded. Both always describe the
workspace's current account, so an envelope sent before the workspace switched account or went live is not
reachable through them.

## Creating a booking

```graphql
mutation CreateBooking($booking: BookingInputType!) {
  bookings {
    create(booking: $booking) {
      booking {
        id
      }
    }
  }
}
```

```json
{
  "booking": {
    "productId": "product-id",
    "unitId": "unit-id",
    "checkIn": "2026-09-15T15:00:00Z",
    "checkOut": "2026-09-20T11:00:00Z",
    "contacts": [
      {
        "contactId": "contact-id",
        "role": "LEAD_GUEST"
      }
    ]
  }
}
```

**Input fields:**

| Field | Type | Description |
|-------|------|-------------|
| `productId` | `ID!` | The product to book |
| `unitId` | `ID` | The unit to book. When omitted, a unit is auto-allocated from the product's active units. |
| `checkIn` | `ISO8601DateTime!` | The check-in date and time |
| `checkOut` | `ISO8601DateTime!` | The check-out date and time |
| `contacts` | `[ContactInputType!]!` | The contacts for the booking |
| `checklistItems` | `[ChecklistItemInputType!]` | Additional checklist items to create alongside the ones generated from the product group's checklist template. These two sets are not deduplicated. |

**Error codes:**

| Code | When |
|------|------|
| `product_not_found_error` | No product matches `productId` |
| `product_not_available_for_booking_error` | The product cannot currently be booked |
| `product_does_not_contain_units_error` | The product has no units to allocate |
| `product_group_not_found_error` | The product's parent product group could not be resolved |
| `unit_not_found_error` | `unitId` was supplied but does not match a unit |
| `unit_does_not_belong_to_unit_type_error` | The supplied unit does not belong to the product's unit type |
| `find_unit_error` | The unit lookup failed |
| `no_units_available` | No unit could be allocated for the requested dates |
| `no_units_allocated_to_pool_error` | No units are allocated to the availability pool |
| `unit_not_allocated_to_product` | The unit is not allocated to the requested product |
| `unit_not_available` | The requested unit is not available for the requested dates |
| `pool_not_found_error` | The availability pool could not be resolved |
| `crm_contact_not_found_error` | A supplied contact ID does not resolve to a CRM contact |
| `lead_guest_not_found_error` | No lead guest was found among the supplied contacts |
| `update_contact_error` | Updating a supplied contact failed |
| `start_date_range_error` / `end_date_range_error` | `checkIn` or `checkOut` falls outside an allowed range |
| `invalid_date_error` / `invalid_date_range_error` / `date_parsing_error` | The supplied dates are invalid or could not be parsed |
| `rate_not_found` | No applicable rate could be resolved for the stay |
| `create_checklist_item_error` | A supplied `checklistItems` entry could not be created |
| `checklist_template_read_error` | The product group's checklist template could not be read |
| `item_limit_exceeded_error` | Too many checklist items were supplied |
| `create_booking_error` | The booking could not be created for another reason |
| `user_not_allowed_error` | The caller lacks permission to create the booking |

## Tentative bookings

Confirm a tentative booking:

```graphql
mutation ConfirmTentativeBooking($confirmTentativeBooking: ConfirmTentativeBookingInputType!) {
  bookings {
    confirmTentativeBooking(confirmTentativeBooking: $confirmTentativeBooking) {
      booking {
        id
      }
    }
  }
}
```

```json
{
  "confirmTentativeBooking": {
    "bookingId": "booking-id"
  }
}
```

Expire a tentative booking early:

```graphql
mutation ExpireTentativeBooking($expireTentativeBooking: ExpireTentativeBookingInputType!) {
  bookings {
    expireTentativeBooking(expireTentativeBooking: $expireTentativeBooking) {
      booking {
        id
      }
    }
  }
}
```

```json
{
  "expireTentativeBooking": {
    "bookingId": "booking-id"
  }
}
```

**Error codes** (shared by both operations): `tentative_booking_not_found`, `booking_not_tentative_error`,
`booking_expired_error`, `failed_to_update_booking_status`, `booking_ttl_not_found`, `finding_job_error`,
`deleting_job_error`, `failed_to_update_booking_ttl`, `processing_error`, `user_not_allowed_error`, plus
`booking_not_expired` and `failed_to_update_booking_ttl_expiry` for expiry only.

> **Tip:** `updateTentativeBookingStatus` is the general-purpose version of these two operations, taking an explicit
> `status: CONFIRMED` or `status: TENTATIVE_EXPIRED`. Prefer `confirmTentativeBooking` and `expireTentativeBooking`
> for their clearer intent.

## Recording arrival and departure

Marking a resident arrived or departed stamps `arrivedAt` / `departedAt` and updates `occupancyStatus`. Both default
to the current time; pass `at` to record a different time. Calling either mutation again once the resident is
already marked arrived (or departed) is a no-op: it returns the booking unchanged and does not update the
timestamp, even if the second call passes a different `at`.

`markArrived` refuses to record an arrival before the contract starts. If the arrival (`at`, or now) or the current
time falls on a day before the booking's `checkIn` date, it returns `check_in_date_not_reached`. Days are counted in
the property's timezone, so a check-in on the `checkIn` day itself is always accepted. Neither mutation otherwise
checks the booking's status or its scheduled dates, so, for example, `markDeparted` can be called on a booking that
was never marked arrived.

```graphql
mutation MarkArrived($input: MarkArrivedMutationInput!) {
  bookings {
    markArrived(input: $input) {
      booking {
        id
        occupancyStatus
        checkedInStatus
        arrivedAt
      }
    }
  }
}
```

```json
{
  "input": {
    "bookingId": "booking-id"
  }
}
```

```graphql
mutation MarkDeparted($input: MarkDepartedMutationInput!) {
  bookings {
    markDeparted(input: $input) {
      booking {
        id
        occupancyStatus
        checkedInStatus
        departedAt
      }
    }
  }
}
```

```json
{
  "input": {
    "bookingId": "booking-id"
  }
}
```

Both input types share these key fields (the description refers to arrival for `MarkArrivedMutationInput` and
departure for `MarkDepartedMutationInput`):

| Field | Type | Description |
|-------|------|-------------|
| `bookingId` | `ID!` | The booking whose resident arrived (or departed) |
| `at` | `ISO8601DateTime` | When the resident physically arrived (or departed); defaults to now |

> **Note:** Marking a booking arrived or departed only ever sets `occupancyStatus` to `CHECKED_IN` or `CHECKED_OUT`.
> `NO_SHOW` and `OVERSTAY` are set automatically when a booking misses its check-in or check-out window; `AWAY` is
> not currently set by anything.

**Error codes:** `booking_not_found`, `check_in_date_not_reached` (`markArrived` only),
`failed_to_mark_booking_arrived` (or `failed_to_mark_booking_departed`), `failed_to_publish_kafka_message`,
`user_not_allowed_error`, `validation_error`.

## Setting scheduled move dates

`setScheduledMoveDates` overrides a booking's planned physical move-in and move-out date and time, independent of
the contract window. Passing `null` for either field clears the override, reverting that field to the contract
default (see [Three layers of dates](#three-layers-of-dates-contract-scheduled-and-actual)).

> **Note:** Rescheduling `scheduledMoveInAt` on a booking currently marked `NO_SHOW` clears that occupancy status,
> so the booking is re-evaluated against the new date rather than staying stuck as a no-show.

```graphql
mutation SetScheduledMoveDates(
  $bookingId: ID!
  $scheduledMoveInAt: ISO8601DateTime
  $scheduledMoveOutAt: ISO8601DateTime
) {
  bookings {
    setScheduledMoveDates(
      bookingId: $bookingId
      scheduledMoveInAt: $scheduledMoveInAt
      scheduledMoveOutAt: $scheduledMoveOutAt
    ) {
      booking {
        id
        scheduledMoveInAt
        scheduledMoveOutAt
        occupancyStatus
      }
    }
  }
}
```

```json
{
  "bookingId": "booking-id",
  "scheduledMoveInAt": "2026-09-15T18:00:00Z",
  "scheduledMoveOutAt": "2026-09-20T09:00:00Z"
}
```

**Input fields:**

| Field | Type | Description |
|-------|------|-------------|
| `bookingId` | `ID!` | The booking to re-schedule |
| `scheduledMoveInAt` | `ISO8601DateTime` | Planned physical arrival. Omit or pass `null` to clear the override and revert to the contract default. |
| `scheduledMoveOutAt` | `ISO8601DateTime` | Planned physical departure. Omit or pass `null` to clear the override and revert to the contract default. |

**Error codes:**

| Code | When |
|------|------|
| `booking_not_found` | No booking matches `bookingId` |
| `failed_to_set_scheduled_move_dates` | The scheduled move dates could not be saved |
| `user_not_allowed_error` | The caller lacks permission to set scheduled move dates for this booking |
| `validation_error` | The input failed structural validation |

## Canceling a booking

Direct cancellation is immediate and is scoped to direct bookings. For a reviewable cancelation workflow with
separate submit and approve steps, see [Cancelation Requests](/bookings/cancelation-requests/) instead.

```graphql
mutation CancelBooking($cancelBooking: CancelBookingInputType!) {
  bookings {
    cancelBooking(cancelBooking: $cancelBooking) {
      booking {
        id
      }
    }
  }
}
```

```json
{
  "cancelBooking": {
    "bookingId": "booking-id",
    "cancellationReason": "Guest requested cancellation"
  }
}
```

**Input fields:**

| Field | Type | Description |
|-------|------|-------------|
| `bookingId` | `ID!` | The booking to cancel |
| `cancellationReason` | `String` | The reason for cancelling the booking |

**Error codes:**

| Code | When |
|------|------|
| `booking_not_found` | No booking matches `bookingId` |
| `booking_not_local_direct` | The booking is not a direct booking and cannot be canceled this way |
| `booking_not_active` | The booking is not in a cancelable state |
| `internal_booking_cancelation_failed` | The cancellation could not be completed |
| `user_not_allowed_error` | The caller lacks permission to cancel the booking |
| `validation_error` | The input failed structural validation |

## Moving and splitting a booking

> **Note:** Both `moveBooking` and `splitBooking` run asynchronously. The mutation itself returns immediately with
> the booking ID(s) once the request is accepted; it does not confirm the move or split has completed. Re-query the
> booking's `accommodations` to confirm the outcome.

```graphql
mutation MoveBooking($bookingMove: MoveBookingInputType!) {
  bookings {
    moveBooking(bookingMove: $bookingMove) {
      bookings
    }
  }
}
```

```json
{
  "bookingMove": {
    "bookingId": "booking-id",
    "fromUnitId": "unit-id-a",
    "toUnitId": "unit-id-b",
    "moveCheckIn": "2026-09-20",
    "moveCheckOut": "2026-09-25",
    "moveReason": "Guest requested a quieter unit"
  }
}
```

`splitBooking` follows the same shape, dividing the stay into segments instead of moving it wholesale:

```graphql
mutation SplitBooking($bookingSplit: SplitBookingInputType!) {
  bookings {
    splitBooking(bookingSplit: $bookingSplit) {
      bookings
    }
  }
}
```

```json
{
  "bookingSplit": {
    "bookingId": "booking-id",
    "splitReason": "Guest requested a room change partway through the stay",
    "segments": [
      { "unitId": "unit-id-a", "checkIn": "2026-09-15", "checkOut": "2026-09-20" },
      { "unitId": "unit-id-b", "checkIn": "2026-09-20", "checkOut": "2026-09-25" }
    ]
  }
}
```

## Other booking mutations

| Mutation | Purpose |
|----------|---------|
| `addContactsToBooking` | Adds guest contacts to a booking (up to 10; only one lead guest allowed) |
| `removeContactFromBooking` | Removes a single contact from a booking |
| `changeBookingLeadGuest` | Changes the lead guest on a booking |
| `updateGroupBookingName` | Renames a group booking |
| `applyDiscount` | Applies a specific coupon-backed discount to a booking |
| `removeDiscount` | Removes a previously applied discount |
| `redeemCoupon` | Redeems a coupon by its code against a booking |
| `addOptionalServices` | Adds one or more optional services to a booking |
| `updateOptionalServices` | Updates an existing optional service on a booking |
| `removeOptionalServices` | Removes optional services from a booking |
| `setRelocationPolicy` | Sets whether a booking may be moved off its current unit (`UNRESTRICTED` or `LOCKED`) |
| `updateBookingTtl` | Updates a tentative booking's time-to-live expiry |
| `startChannelCancellation` | Starts a channel cancellation request for a booking on the originating platform |
| `importBookings` | Bulk-imports a batch of bookings from an external source |

## Checklists

A booking's checklist tracks operational tasks against it, each with a category, type, status, and an optional
guard controlling when it can be transitioned.

```graphql
query BookingChecklist($bookingId: ID!) {
  bookings {
    booking(bookingId: $bookingId) {
      checklist {
        id
        items {
          nodes {
            id
            label
            category
            status
            type
            isEditable
          }
        }
      }
    }
  }
}
```

```json
{
  "bookingId": "booking-id"
}
```

Add items, update a custom item, and transition an item (complete or waive it):

```graphql
mutation CreateChecklistItems($bookingId: ID!, $items: [ChecklistItemInputType!]!) {
  bookings {
    createChecklistItems(bookingId: $bookingId, items: $items) {
      checklist {
        id
      }
    }
  }
}
```

```graphql
mutation TransitionChecklistItem($id: ID!, $input: TransitionChecklistItemInputType!) {
  bookings {
    transitionChecklistItem(id: $id, input: $input) {
      checklistItem {
        id
        status
      }
    }
  }
}
```

```json
{
  "id": "checklist-item-id",
  "input": {
    "transition": "COMPLETE"
  }
}
```

`transition` accepts `COMPLETE` or `WAIVE`. See the [MCP tools](#mcp-tools) below for `get_booking_checklist`,
`create_checklist_items`, and `update_checklist_items`, which cover the checklist surface in full.

## MCP Tools

If you are using the Lavanda MCP integration, the following tools are available for working with bookings:

- [get_booking](/mcp/tools/bookings/get_booking/) - Get detailed booking information by ID
- [search_bookings](/mcp/tools/bookings/search_bookings/) - Search bookings with rich filtering,
  sorting, and pagination
- [update_booking_ttl](/mcp/tools/bookings/update_booking_ttl/) - Update a tentative booking's
  time-to-live
- [get_booking_checklist](/mcp/tools/bookings/get_booking_checklist/) - Get a booking's checklist
- [create_checklist_items](/mcp/tools/bookings/create_checklist_items/) - Add checklist items to a
  booking
- [update_checklist_items](/mcp/tools/bookings/update_checklist_items/) - Update a booking's checklist
  items

See the [MCP Tools](/mcp/tools/) overview for setup instructions and the full list of available tools.

## Webhooks

| Event type | When it fires |
|------------|---------------|
| `v1.booking.created` | A booking is created |
| `v1.booking.confirmed` | A tentative booking is confirmed |
| `v1.booking.expired` | A tentative booking expires (or is explicitly expired) |
| `v1.booking.updated` | A booking is updated |
| `v1.booking.lead_guest_changed` | A booking's lead guest changes |
| `v1.booking.declined` | A booking is declined |
| `v1.booking.canceled` | A booking is canceled |
| `v1.booking.terminated` | A booking is terminated |
| `v1.booking.booking_ttl.updated` | A tentative booking's time-to-live is updated |
| `v1.booking.checklist_item.created` | A checklist item is created |
| `v1.booking.checklist_item.completed` | A checklist item is completed |
| `v1.booking.checklist_item.waived` | A checklist item is waived |

Cancelation and termination requests emit their own event types, documented on their own pages (see
[Key Concepts](#canceling-and-terminating-a-booking) above for links).

Payload shape:

```json
{
  "type": "v1.booking.confirmed",
  "id": "evt_2x...",
  "timestamp": "2026-05-22T13:00:00Z",
  "booking": { "id": "booking-id" },
  "workspace": { "id": "workspace-id" }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `type` | `string` | The event type |
| `id` | `string` | Stable, unique identifier for this event delivery |
| `timestamp` | `string` (ISO 8601) | When the underlying change happened |
| `booking.id` | `string` | The booking the event concerns |
| `workspace.id` | `string` | The workspace that owns the booking |

To resolve any further detail after receiving an event, re-query the booking via the GraphQL API using the
`booking.id` from the payload. See [Webhooks](/concepts/webhooks/) for the general delivery
model and signature verification.

## Relationships

- A booking belongs to a **workspace**, and is made against a **product** (see
  [Products](/catalog/products/)), which allocates it to one or more **units** over time (see
  [Buildings, Unit Types, and Units](/spaces-and-inventory/buildings/))
- A booking has one or more **contacts** (see [Contacts](/crm/contacts/)), typically originating from
  a **lead** (see [Leads](/crm/leads/))
- A booking can have **cancelation requests** and **termination requests** (each at most one pending at a time)
- A booking can have a **checklist** of operational tasks, **discounts**, and **optional services**

See [Structure & Terms](/concepts/structure-and-terms/) for how these entities fit together across the
platform.
