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.
Key Concepts
Section titled “Key Concepts”Lifecycle
Section titled “Lifecycle”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.
The termination date
Section titled “The termination date”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.
Eligibility
Section titled “Eligibility”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
terminationDatefalls 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.
Permissions
Section titled “Permissions”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.
Side effects of approval
Section titled “Side effects of approval”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
PendingtoApproved - 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
terminatedis scheduled for when the curtailed stay ends - The
approvedwebhook 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
confirmedright up to that date and keeps behaving as a normal confirmed booking (it still reportsstatus: CONFIRMED, stays bookable for its curtailed stay, etc.). It only transitions toterminatedwhen 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.
GraphQL API
Section titled “GraphQL API”TerminationRequest (interface)
Section titled “TerminationRequest (interface)”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.
Reading the pending request on a booking
Section titled “Reading the pending request on a booking”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,
terminationRequestsreturns as if no requests exist, regardless of any rows that may have been written before the setting was toggled.
Submitting a termination request
Section titled “Submitting a termination request”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 |
Transitioning a pending request
Section titled “Transitioning a pending request”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 alreadyterminated, the request still transitions toApprovedbut the booking is not terminated again. No duplicate booking-terminated webhook event is sent.
Webhooks
Section titled “Webhooks”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.
Relationships
Section titled “Relationships”Termination requests sit alongside bookings in the inventory hierarchy:
- A booking can have many termination requests over time, but at most one in the
Pendingstate - 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.