Skip to content

Termination Requests

A termination request is an explicit, reviewable record that a booking which has already started should be ended early. Instead of curtailing the booking immediately, a caller with the right permission opens a request against the booking; another caller (or the same one, if they hold both permissions) then approves, declines, or withdraws it. Approving curtails the booking to the requested final date in the same transaction; declining and withdrawing leave the booking confirmed.

Termination is the inverse of a cancelation request. A cancelation applies to a booking whose stay is still in the future, whereas a termination ends a booking whose contract has already started. The two flows are mutually exclusive: a booking cannot have a pending termination request and a pending cancelation request at the same time.

Termination requests are a per-workspace opt-in. When the workspace setting is off, all the operations described on this page short-circuit with termination_approvals_disabled, and the read field returns an empty list.

Every termination request follows the same four-state lifecycle:

submit approve
[None] -------> [Pending] ---------> [Approved] (booking curtailed; termination scheduled)
|
|--- decline ---> [Declined] (booking stays confirmed)
|
|--- withdraw --> [Withdrawn] (booking stays confirmed)

There is at most one pending request per booking at any time. Once a request is in a terminal state (approved, declined, or withdrawn), a new pending request can be opened.

Unlike a cancelation request, a termination request carries a requested final date, the terminationDate. This is the date the started booking should be cut short to, and it must fall within the booking’s contract window (between its check-in and check-out, inclusive). The requested date is captured when the request is submitted and travels with the request through its lifecycle. An approver may supply an amended final date at approval time; if they do, that amended date is the one the booking is curtailed to.

A booking is eligible for a termination request when all of the following hold:

  • The workspace setting for termination requests is enabled
  • The booking is a student Product Booking
  • The booking status is confirmed
  • The booking contract has started (its check-in is on or before today)
  • The booking does not already have a pending cancelation request
  • The booking does not already have a pending termination request
  • The requested terminationDate falls within the contract window

If a booking is not a student Product Booking, is not confirmed, or has not yet started, submitTerminationRequest returns booking_not_eligible_for_termination_approval. A requested date outside the contract window returns termination_date_out_of_bounds, and a competing pending cancelation request returns booking_has_pending_cancelation_request. Eligibility is only checked on submit; transitions on an already-pending request are not re-gated by eligibility, though an amended date supplied at approval time is bounds-checked again.

Authorization is checked per operation, against the booking resource:

Operation Required permission on the booking
Submit a termination request request_termination
Approve a pending request approve_termination
Decline a pending request decline_termination
Withdraw a pending request withdraw_termination

A caller who lacks the matching permission gets back unauthorized, regardless of the state of the booking or any pending request.

When a pending request is approved, the booking is curtailed and the termination is scheduled inside the same database transaction as the request status flip. The booking is not flipped to terminated at approval time — it stays confirmed until the scheduled termination is applied. Concretely, on APPROVE:

  • The request moves from Pending to Approved
  • The booking is curtailed to the final termination date (the amended date if the approver supplied one, otherwise the date captured at submit time), and a flip to terminated is scheduled for when the curtailed stay ends
  • The approved webhook event for the request fires in this transaction

The booking’s status flips to terminated — and the v1.booking.terminated event fires — only when that scheduled termination is applied: effectively immediately if the final date is in the past or today, or deferred until that date if it is in the future. So approval does not always terminate the booking immediately.

Note: When the approved termination date is in the future, the booking remains confirmed right up to that date and keeps behaving as a normal confirmed booking (it still reports status: CONFIRMED, stays bookable for its curtailed stay, etc.). It only transitions to terminated when the scheduled date arrives. A past or same-day approved date applies effectively immediately.

If the booking is already terminated when the approval lands, the request still moves to Approved, but the curtailment/scheduling side effect is skipped, so re-approving is idempotent.

The polymorphic interface every request implements. Reads return this interface; clients use inline fragments to pull state-specific timestamps.

Field Type Description
status ApprovalRequestStatusEnum! Current lifecycle state of the request
requestedAt ISO8601DateTime! When the request was first submitted
terminationDate ISO8601Date! The requested final date for the booking
terminationReason BookingTerminationReasonEnum Optional reason the caller attached at submit time

The owning workspace is reachable via the booking itself (booking.workspace { id }); a termination request always belongs to the same workspace as its booking.

The interface has four concrete implementations, one per lifecycle state. Each adds a single state-specific timestamp:

Concrete type Extra field
PendingTerminationRequest (interface fields only)
ApprovedTerminationRequest approvedAt: ISO8601DateTime!
DeclinedTerminationRequest declinedAt: ISO8601DateTime!
WithdrawnTerminationRequest withdrawnAt: ISO8601DateTime!

Decision timestamps are state-specific by design: a PendingTerminationRequest will not carry an approvedAt, and an ApprovedTerminationRequest will not carry a declinedAt. Use inline fragments to pull whichever timestamp matches the request’s current state (see the query examples below).

ApprovalRequestStatusEnum - lifecycle state of a termination request:

Value Description
PENDING The request has been submitted and is awaiting a decision
APPROVED The request has been approved
DECLINED The request has been declined
WITHDRAWN The request was withdrawn before a decision was made

ApprovalRequestTransitionEnum - the event applied to a pending request:

Value Description
APPROVE Approve the pending request (also terminates the booking)
DECLINE Decline the pending request
WITHDRAW Withdraw the pending request

BookingTerminationReasonEnum - the reason a caller may attach when submitting a request. Values are:

ABSCONDER, ACADEMIC, CAMPUS_CHANGE, COMPASSIONATE, COMPLAINT, EVICTION, FINANCIAL, HOUSEKEEPING_ISSUE, INCIDENT, LOCATION, MAINTENANCE_ISSUE, MEDICAL, NO_SHOW, NOISE_ISSUE, OTHER, PROCESSING_ERROR, PROPERTY_SOLD, PROPERTY_TRANSFER, SOCIAL, WELLBEING.

Note: The termination reason set is distinct from the cancelation reason set. The two workflows track different operational reasons, so they do not share an enum.

Read the pending request from terminationRequests(status: [PENDING]). Because a booking can have at most one pending termination request at a time, filtering the connection to PENDING and reading the first node gives you the current open request, or no node when none exists.

query BookingPendingTerminationRequest($id: ID!) {
bookingAvailability {
booking(id: $id) {
id
terminationRequests(status: [PENDING]) {
nodes {
__typename
status
requestedAt
terminationDate
terminationReason
}
}
}
}
}
{
"id": "booking-id"
}

nodes will contain a single PendingTerminationRequest when a pending request exists, and be empty when one does not.

Reading the full request history on a booking

Section titled “Reading the full request history on a booking”

Booking.terminationRequests(status: [...]) returns the full list of requests for the booking, newest first. It follows the standard pagination pattern used across the API, so the connection exposes nodes, edges, pageInfo, and totalCount. Pass status: to narrow the result to one or more lifecycle states; omit it to get everything.

Because the connection’s nodes are typed as the TerminationRequest interface, you use inline fragments to pull the state-specific decision timestamp.

query BookingTerminationRequests($id: ID!) {
bookingAvailability {
booking(id: $id) {
id
terminationRequests {
totalCount
nodes {
__typename
status
requestedAt
terminationDate
terminationReason
... on ApprovedTerminationRequest {
approvedAt
}
... on DeclinedTerminationRequest {
declinedAt
}
... on WithdrawnTerminationRequest {
withdrawnAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
{
"id": "booking-id"
}

To filter to a single state, supply status:

query BookingApprovedTerminationRequests($id: ID!, $status: [ApprovalRequestStatusEnum!]) {
bookingAvailability {
booking(id: $id) {
terminationRequests(status: $status) {
nodes {
__typename
terminationDate
... on ApprovedTerminationRequest {
approvedAt
}
}
}
}
}
}
{
"id": "booking-id",
"status": ["APPROVED"]
}

Note: When the workspace setting for termination requests is off, terminationRequests returns as if no requests exist, regardless of any rows that may have been written before the setting was toggled.

Use the submitTerminationRequest mutation under the bookings namespace. The request is keyed by bookingId; at most one pending request can exist per booking, so you do not (and cannot) supply a request id. Supply the requested final date in terminationDate.

mutation SubmitTerminationRequest($input: SubmitTerminationRequestInput!) {
bookingAvailability {
bookings {
submitTerminationRequest(submitTerminationRequest: $input) {
booking {
id
status
terminationRequests(status: [PENDING]) {
nodes {
__typename
status
requestedAt
terminationDate
terminationReason
}
}
}
}
}
}
}
{
"input": {
"bookingId": "booking-id",
"terminationDate": "2026-09-30",
"terminationReason": "ABSCONDER"
}
}

Input fields:

Field Type Description
bookingId ID! The booking to open a termination request against
terminationDate ISO8601Date! The requested final date for the booking. Must fall within the contract window
terminationReason BookingTerminationReasonEnum Optional reason, surfaced on the request

Error codes:

Code When
validation_error The input failed structural validation (for example, a missing or malformed terminationDate)
termination_approvals_disabled The workspace setting is off
booking_not_found No booking matches bookingId
unauthorized The caller lacks request_termination on the booking
booking_not_eligible_for_termination_approval The booking fails one of the eligibility checks (see Eligibility)
termination_date_out_of_bounds The requested terminationDate is outside the booking contract window
booking_has_pending_cancelation_request The booking already has a pending cancelation request
pending_cancelation_request_check_failed The check for a competing pending cancelation request could not be completed
termination_request_already_pending A pending termination request for this booking already exists

Use the transitionTerminationRequest mutation to approve, decline, or withdraw the pending request on a booking. The request is again keyed by bookingId, since at most one pending request can exist per booking. On APPROVE only, you may supply an amended terminationDate; supplying it with DECLINE or WITHDRAW is rejected because those transitions apply no date.

mutation TransitionTerminationRequest($input: TransitionTerminationRequestInput!) {
bookingAvailability {
bookings {
transitionTerminationRequest(transitionTerminationRequest: $input) {
booking {
id
status
terminationRequests(status: [APPROVED, DECLINED, WITHDRAWN]) {
nodes {
__typename
terminationDate
... on ApprovedTerminationRequest { approvedAt }
... on DeclinedTerminationRequest { declinedAt }
... on WithdrawnTerminationRequest { withdrawnAt }
}
}
}
}
}
}
}

Approve, accepting the date captured at submit time:

{
"input": {
"bookingId": "booking-id",
"transition": "APPROVE"
}
}

Approve with an amended final date:

{
"input": {
"bookingId": "booking-id",
"transition": "APPROVE",
"terminationDate": "2026-10-15"
}
}

Decline:

{
"input": {
"bookingId": "booking-id",
"transition": "DECLINE"
}
}

Withdraw:

{
"input": {
"bookingId": "booking-id",
"transition": "WITHDRAW"
}
}

Input fields:

Field Type Description
bookingId ID! The booking whose pending request is being transitioned
transition ApprovalRequestTransitionEnum! One of APPROVE, DECLINE, WITHDRAW
terminationDate ISO8601Date Optional amended final date, only valid when approving. Defaults to the date captured at submit time

Error codes:

Code When
termination_approvals_disabled The workspace setting is off
booking_not_found No booking matches bookingId
unauthorized The caller lacks the per-transition permission (approve_termination, decline_termination, or withdraw_termination) on the booking
termination_request_not_pending No pending request exists for this booking (it may have been transitioned already, or it may never have been opened)
termination_date_missing A final termination date is required to approve but none is available
termination_date_invalid The supplied amended date could not be parsed
termination_date_out_of_bounds The supplied amended date is outside the booking contract window

Note: On APPROVE, if the booking is already terminated, the request still transitions to Approved but the booking is not terminated again. No duplicate booking-terminated webhook event is sent.

Each lifecycle change emits a webhook event. Approving emits the approved event for the request in the approval transaction; the v1.booking.terminated event is emitted separately when the scheduled termination is applied, which may be later than the approval (see Side effects of approval). Declining and withdrawing emit only their own event; the booking stays confirmed.

Event type When it fires
v1.booking.termination_request.requested A new pending request is opened via submitTerminationRequest
v1.booking.termination_request.approved A pending request is approved via transitionTerminationRequest (v1.booking.terminated follows when the scheduled termination is applied, unless the booking was already terminated)
v1.booking.termination_request.declined A pending request is declined via transitionTerminationRequest
v1.booking.termination_request.withdrawn A pending request is withdrawn via transitionTerminationRequest
v1.booking.terminated A booking’s scheduled termination is applied and its status flips to terminated (immediately for a past/today final date, later for a future one)

The four termination_request.* events share the same payload shape:

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

Payload fields:

Field Type Description
type string The event type (one of the four termination_request.* values above)
id string Stable, unique identifier for this event delivery
timestamp string (ISO 8601) When the underlying lifecycle change happened
booking.id string The booking the request targets
workspace.id string The workspace that owns the booking

The v1.booking.terminated event carries the time the booking was terminated in addition to the booking and workspace identifiers:

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

To resolve any further detail about the request or the booking after receiving an event, re-query the booking via the GraphQL API using the booking.id from the payload. The read field documented above is the canonical source of truth.

See Webhooks for the general webhook delivery model and signature verification.

Termination requests sit alongside bookings in the inventory hierarchy:

  • A booking can have many termination requests over time, but at most one in the Pending state
  • A termination request always belongs to exactly one workspace (the booking’s workspace) and exactly one booking

See Structure & Terms for more on the booking model.