From e7540dccea30b5628404a4bf3eb359dd32d85bec Mon Sep 17 00:00:00 2001 From: Jing Li Date: Fri, 28 Aug 2026 20:02:20 +0000 Subject: [PATCH 1/8] Introduce Booking capability for the lodging industry. --- .cspell/custom-words.txt | 4 + docs/specification/lodging/booking/index.md | 432 ++++++ docs/specification/lodging/booking/mcp.md | 1107 ++++++++++++++ docs/specification/lodging/booking/rest.md | 1362 +++++++++++++++++ .../lodging/extensions/cancellation-policy.md | 161 ++ docs/specification/reference.md | 12 + main.py | 51 +- mkdocs.yml | 24 + .../lodging_booking_request_complete.json | 15 + .../lodging_booking_request_create.json | 24 + .../lodging_booking_request_update.json | 37 + .../scaffolds/lodging_booking_response.json | 79 + .../schemas/common/payment_ap2_mandate.json | 29 + .../common/payment_authentication.json | 127 +- .../common/payment_split_payments.json | 29 + source/schemas/common/payment_terms.json | 21 + .../schemas/common/types/date_interval.json | 18 + source/schemas/lodging/booking.json | 179 +++ .../schemas/lodging/policy_cancellation.json | 60 + .../schemas/lodging/types/accommodation.json | 26 + source/schemas/lodging/types/booker.json | 40 + .../lodging/types/booking_confirmation.json | 29 + source/schemas/lodging/types/capacity.json | 43 + source/schemas/lodging/types/guest.json | 37 + .../lodging/types/guest_assignment.json | 21 + source/schemas/lodging/types/occupancy.json | 37 + source/schemas/lodging/types/rate_plan.json | 33 + source/schemas/lodging/types/room_rate.json | 89 ++ source/schemas/lodging/types/room_type.json | 47 + source/schemas/ucp.json | 30 + source/services/lodging/mcp.openrpc.json | 211 +++ source/services/lodging/rest.openapi.json | 586 +++++++ 32 files changed, 4934 insertions(+), 66 deletions(-) create mode 100644 docs/specification/lodging/booking/index.md create mode 100644 docs/specification/lodging/booking/mcp.md create mode 100644 docs/specification/lodging/booking/rest.md create mode 100644 docs/specification/lodging/extensions/cancellation-policy.md create mode 100644 scripts/scaffolds/lodging_booking_request_complete.json create mode 100644 scripts/scaffolds/lodging_booking_request_create.json create mode 100644 scripts/scaffolds/lodging_booking_request_update.json create mode 100644 scripts/scaffolds/lodging_booking_response.json create mode 100644 source/schemas/common/types/date_interval.json create mode 100644 source/schemas/lodging/booking.json create mode 100644 source/schemas/lodging/policy_cancellation.json create mode 100644 source/schemas/lodging/types/accommodation.json create mode 100644 source/schemas/lodging/types/booker.json create mode 100644 source/schemas/lodging/types/booking_confirmation.json create mode 100644 source/schemas/lodging/types/capacity.json create mode 100644 source/schemas/lodging/types/guest.json create mode 100644 source/schemas/lodging/types/guest_assignment.json create mode 100644 source/schemas/lodging/types/occupancy.json create mode 100644 source/schemas/lodging/types/rate_plan.json create mode 100644 source/schemas/lodging/types/room_rate.json create mode 100644 source/schemas/lodging/types/room_type.json create mode 100644 source/services/lodging/mcp.openrpc.json create mode 100644 source/services/lodging/rest.openapi.json diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index eef7fc92d..b8a434b39 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -182,3 +182,7 @@ Honeycrisp unrepresentable wifi optionalities +booker +pincode +refundability +Refundability diff --git a/docs/specification/lodging/booking/index.md b/docs/specification/lodging/booking/index.md new file mode 100644 index 000000000..2ae581448 --- /dev/null +++ b/docs/specification/lodging/booking/index.md @@ -0,0 +1,432 @@ + + +# Booking Capability + +* **Capability Name:** `dev.ucp.lodging.booking` + +## Overview + +The Lodging Booking capability allows Platforms to facilitate and manage end-to-end +lodging reservation sessions with Businesses. + +The Business remains the Merchant of Record (MoR) and does not need to become +PCI DSS compliant to accept card payments through this capability. Unless the +AP2 Mandates extension is supported, the booking must be finalized manually +by the user through a trusted UI. + +### Flow Overview + +Booking follows a progressive session lifecycle: + +1. **Session Initiation**: The Platform initiates a booking session using room, + rate, and itinerary details discovered from upper-funnel search. +2. **Progressive Enrichment**: The Platform updates the session with guest + profiles, room assignments, booker information, and payment details across + one or more operations. +3. **Session Completion**: The Platform finalizes the booking to create a + confirmed, immutable reservation. + +```text + +------------+ +---------------------+ + | incomplete |<----------------------->| requires_escalation | + +-----+------+ | (user handoff | + | | via continue_url) | + | all info collected +----------+----------+ + v | + +------------------+ | + |ready_for_complete| | + | | | + | (platform can | | continue_url + | call Complete | | + | Booking Session) | | + +--------+---------+ | + | | + | Complete Booking | + v | + +--------------------+ | + |complete_in_progress| | + +---------+----------+ | + | | + +-----------------------+-------------------+ + v + +-------------+ + | completed | + +-------------+ + + +-------------+ + | canceled | + +-------------+ + (session invalid/expired - can occur from any state) +``` + +## Key Concepts + +* **Compound Room Rate Binding (`room_rate`)**: A lodging reservation is composed + of one or more room rate units. A `room_rate` is a compound binding linking + a physical room type (`room_type`), a commercial rate contract + (`rate_plan`), occupancy requirements (`occupancy`), and guest room + assignments (`guest_assignments`). +* **Platform-Generated Guest Identifiers (`guest.id`)**: Unlike business-scoped + catalog and room identifiers, guest identifiers are generated, allocated, and + managed by the Platform within the Platform's namespace. The Business treats + `guest.id` as a stable, opaque reference. +* **Guest Pool & Room Assignment Model**: Guest data is structured into a + two-level relational model: + * **Root Guest Pool (`guests[]`)**: A flat collection of all individual guest + profiles associated with the entire reservation. + * **Room Assignments (`room_rates[].guest_assignments[]`)**: Granular mappings + associating specific room units with guests from the root pool via + `guest_id` and designating occupancy roles (such as `primary_guest` or + `additional_guest`). +* **Separation of Booker and Guests**: The data model strictly separates the + legal purchaser from the physical room occupants: + * **`booker`**: The legal contracting party responsible for payment, contact + obligations, and reservation ownership. + * **`guests`**: The individuals who will physically occupy the accommodations. + A booker **MAY** also be listed as a guest in the root pool, but the entities + remain decoupled to support corporate, proxy, and multi-room bookings. +* **Provisional Discovery vs. Authoritative Booking**: + * *Discovery Phase (Provisional)*: Search, quotation, and room lookup + responses provide provisional rates, available room types, and policy + summaries based on search parameters. + * *Booking Session (Authoritative)*: Creating a booking session transitions + from provisional discovery to an authoritative state. The Business locks + or evaluates real-time inventory, resolves binding rate rules, enforces + room capacity bounds, calculates totals (`totals[]`), and attaches + authoritative cancellation terms (`policies[]`). + +### Payments + +Payment handlers are discovered from the business's UCP profile at +`/.well-known/ucp`. The handlers define the processing specifications for +collecting payment instruments (e.g., Google Pay, Shop Pay). When the user +submits payment, the platform populates the `payment.instruments` array with the +collected instrument data. + +The `payment` object is optional on booking creation and may be omitted for +use cases that don't require immediate payment processing (e.g., pay after +arrival or hold-with-card). + +### Booking Status Lifecycle + +The booking `status` field indicates the current phase of the session and +determines what action is required next. The business sets the status; the +platform receives messages indicating what's needed to progress. + +#### Status Values + +* **`incomplete`**: Booking session is missing required information or has + issues that need resolution. Platform should inspect `messages` array for + context and should attempt to resolve via Update Booking Session. +* **`requires_escalation`**: Booking session requires information that + cannot be provided via API, or user input is required. Platform should + inspect `messages` to understand what's needed. If any `recoverable` errors + exist, resolve those first. Then hand off to user via `continue_url`. +* **`ready_for_complete`**: Booking session has all necessary information + and platform can finalize programmatically. Platform can call + Complete Booking Session. +* **`complete_in_progress`**: Business is processing the Complete Booking + request. +* **`completed`**: Booking confirmed successfully. +* **`canceled`**: Booking session is invalid or expired. Platform should + start a new booking session if needed. + +### Error Handling + +The `messages` array contains errors, warnings, and informational messages +about the booking state. `ucp.status` is the shape discriminator — +`"success"` means the response carries the expected payload, `"error"` +means it carries error information instead. The `severity` field on each +error message prescribes the recommended action: + +| Severity | Meaning | Platform Action | +| :---------------------- | :----------------------------------------------- | :---------------------------------------------------------------- | +| `recoverable` | Platform can resolve by modifying inputs via API | Update resource and retry | +| `requires_buyer_input` | Business requires input not available via API | Hand off via `continue_url` | +| `requires_buyer_review` | User review and authorization is required | Hand off via `continue_url` | +| `unrecoverable` | No resource exists to act on | Retry with new resource or inputs, or hand off via `continue_url` | + +Errors with `requires_*` severity contribute to `status: requires_escalation`. +Both result in user handoff, but represent different booking session states: + +* `requires_buyer_input` means the booking session is **incomplete** — the business + requires information their API doesn't support collecting programmatically. +* `requires_buyer_review` means the booking session is **complete** — but policy, + regulatory, or entitlement rules require user authorization before completion. + +#### Standard Errors + +| Code | Description | +| :-------------------- | :--------------------------------------------------------- | +| `inventory_exhausted` | The selected room or inventory hold is no longer available | +| `payment_failed` | Payment processing failed | +| `eligibility_invalid` | Eligibility claim could not be verified at completion | + +### Warning Presentation + +The `presentation` field on warning messages controls the rendering +contract the platform **MUST** follow. When omitted, it defaults to +`"notice"`. + +| | `notice` (default) | `disclosure` | +| :----------------------- | :----------------- | :-------------------------- | +| Display content | **MUST** | **MUST** | +| Proximity to `path` | **MAY** | **MUST** | +| Dismissible | **MAY** | **MUST NOT** | +| Render `image_url` | **MAY** | **MUST** | +| Render `url` | **MAY** | **SHOULD** | +| Escalate if cannot honor | — | **MUST** via `continue_url` | + +#### `notice` (default) + +The default rendering contract for warnings. Platforms **MUST** display +the warning content to the user. Platforms **MAY** render notices in a +banner, tray, or toast, and **MAY** allow the user to dismiss them. + +#### `disclosure` + +Warnings with `presentation: "disclosure"` carry notices — additional +terms & policies, compliance content, etc. — that **MUST** follow the +prescribed rendering contract. + +## Continue URL + +The `continue_url` field enables booking handoff from platform to business UI, +allowing the user to continue and finalize the booking session. + +### Availability + +Businesses **MUST** provide `continue_url` when returning `status` = +`requires_escalation`. For all other non-terminal statuses (`incomplete`, +`ready_for_complete`, `complete_in_progress`), businesses **SHOULD** provide +`continue_url`. For terminal states (`completed`, `canceled`), `continue_url` +**SHOULD** be omitted. + +## Guidelines + +### Platform + +* **MUST** supply valid `accommodation.id`, `room_rate.id`, `room_details.id`, + and `rate_plan.id` identifiers sourced from upper-funnel discovery mechanisms + when creating a booking session. +* **MUST** generate unique, stable string identifiers in the Platform namespace + for each entry in the root `guests[]` array (e.g., `"gst_01"`, `"gst_02"`). +* **MUST** ensure every `guest_assignments[].guest_id` references a valid `id` + present in the root `guests[]` pool. +* **MAY** engage an agent to facilitate the booking session (e.g. select room, + dates, collect guest information). However, the agent must hand over + the booking session to a trusted and deterministic UI for the user to review + the booking details and complete the booking. +* **MAY** send the user from the trusted, deterministic UI back to the agent + at any time. +* **MAY** provide agent context when the platform indicates that the request + was done by an agent. +* **MUST** use `continue_url` when booking status is `requires_escalation`. +* **MAY** use `continue_url` to hand off to business UI in other situations. +* When performing handoff, **SHOULD** prefer business-provided `continue_url`. + +### Business + +* **MUST** evaluate requested `room_rate.id`, `room_details.id`, and `rate_plan.id` + bindings against real-time availability and inventory constraints, echoing + authoritative room metadata, pricing totals, and policy terms. +* **MUST** preserve platform-supplied `guest.id` identifiers across session + updates and responses without remapping, renaming, or mutating them. +* **MUST** validate that all `room_rates[].guest_assignments[].guest_id` + references match an existing entry in the root `guests[]` array. +* **MUST** enforce physical room `capacity` limits against the total assigned + occupants and guest ages. +* **MUST** send a confirmation email after the booking has been completed. +* **SHOULD** provide accurate error and warning messages. +* Logic handling the booking sessions **MUST** be deterministic. +* **MUST** provide `continue_url` when returning `status` = `requires_escalation`. +* **MUST** include at least one message with `severity` of `requires_buyer_input` + or `requires_buyer_review` when returning `status` = `requires_escalation`. +* **SHOULD** provide `continue_url` in all non-terminal booking responses. +* After a booking session reaches the state "completed", it is considered + immutable. + +## Capability Schema Definition + +{{ schema_fields('booking_resp', 'lodging/booking') }} + +## Operations + +The Booking capability defines the following logical operations: + +| Operation | Description | +| :--------------------------- | :---------------------------------------------------------------------------------- | +| **Create Booking Session** | Initiates a new booking session. Called as soon as a user expresses booking intent. | +| **Get Booking Session** | Retrieves the current state of a booking session. | +| **Update Booking Session** | Updates a booking session via full resource replacement. | +| **Complete Booking Session** | Finalizes the booking and confirms the reservation. | +| **Cancel Booking Session** | Cancels a booking session. | + +### Create Booking Session + +Invoked by the platform when the user expresses booking intent to initiate a +session with upper-funnel room, rate, and itinerary parameters. + +{{ method_fields('create_booking_session', 'lodging/rest.openapi.json', 'lodging/booking') }} + +### Get Booking Session + +Retrieves the latest state of the booking session resource. + +{{ method_fields('get_booking_session', 'lodging/rest.openapi.json', 'lodging/booking') }} + +### Update Booking Session + +Performs a full replacement of the booking session resource. The platform is +**REQUIRED** to send the complete booking state containing any data updates +(e.g., guest profiles, room assignments, booker details). + +{{ method_fields('update_booking_session', 'lodging/rest.openapi.json', 'lodging/booking') }} + +### Complete Booking Session + +Final booking placement call. Invoked when payment has been collected and the +user commits to finalize the reservation. + +{{ method_fields('complete_booking_session', 'lodging/rest.openapi.json', 'lodging/booking') }} + +### Cancel Booking Session + +Cancels an active booking session prior to completion. + +{{ method_fields('cancel_booking_session', 'lodging/rest.openapi.json', 'lodging/booking') }} + +## Transport Bindings + +The abstract operations above are bound to specific transport protocols: + +* [REST Binding](rest.md): RESTful API mapping using standard HTTP verbs and JSON payloads. +* [MCP Binding](mcp.md): Model Context Protocol mapping for agentic interaction. + +## Entities + +### Accommodation + +Physical property details associated with the reservation. + +{{ schema_fields('types/accommodation_resp', 'lodging/booking') }} + +### Booking Confirmation + +Confirmation and locator details returned upon successful booking completion. + +{{ schema_fields('types/booking_confirmation', 'lodging/booking') }} + +### Booker + +The legal contracting party and primary point of contact making the reservation. + +{{ schema_fields('types/booker', 'lodging/booking') }} + +### Capacity + +Occupancy limits and child age thresholds supported by a physical room type. + +{{ schema_fields('types/capacity', 'lodging/booking') }} + +### Context + +Buyer location and market context hints. + +{{ schema_fields('types/context', 'lodging/booking') }} + +### Date Interval + +Check-in (`start_date`) and check-out (`end_date`) date range for the stay. + +{{ schema_fields('types/date_interval', 'lodging/booking') }} + +### Guest + +Individual guest profile. The `id` is generated and supplied by the Platform to +uniquely identify the occupant within the booking session. + +{{ schema_fields('types/guest', 'lodging/booking') }} + +### Guest Assignment + +Relational link mapping a room unit to an occupant from the root `guests[]` pool +via `guest_id` with a designated role. + +{{ schema_fields('types/guest_assignment', 'lodging/booking') }} + +### Link + +Compliance and legal links (e.g., Privacy Policy, Terms of Service). + +{{ schema_fields('types/link', 'lodging/booking') }} + +### Message Error + +{{ schema_fields('types/message_error', 'lodging/booking') }} + +### Message Info + +{{ schema_fields('types/message_info', 'lodging/booking') }} + +### Message Warning + +{{ schema_fields('types/message_warning', 'lodging/booking') }} + +### Occupancy + +Requested adult and child guest count breakdown for a room. + +{{ schema_fields('types/occupancy', 'lodging/booking') }} + +### Payment + +Payment details and collected payment instruments. + +{{ schema_fields('payment', 'lodging/booking') }} + +### Rate Plan + +Commercial rate plan contract, cancellation policy rules, and rate inclusions. +The `id` is discovered from upper-funnel search. + +{{ schema_fields('types/rate_plan', 'lodging/booking') }} + +### Room Rate + +Compound binding uniting physical room real estate (`room_details.id`), +commercial rate terms (`rate_plan.id`), occupancy, and guest assignments. + +{{ schema_fields('types/room_rate', 'lodging/booking') }} + +### Room Type + +Physical room real estate attributes and capacity limits. + +{{ schema_fields('types/room_type', 'lodging/booking') }} + +### Signals + +Platform-supplied fraud and security context. + +{{ schema_fields('types/signals', 'lodging/booking') }} + +### Total + +Authoritative itemized price components and aggregate booking total. + +{{ schema_fields('types/total_resp', 'lodging/booking') }} diff --git a/docs/specification/lodging/booking/mcp.md b/docs/specification/lodging/booking/mcp.md new file mode 100644 index 000000000..76b7094bd --- /dev/null +++ b/docs/specification/lodging/booking/mcp.md @@ -0,0 +1,1107 @@ + + +# Booking Capability - MCP Binding + +This document specifies the Model Context Protocol (MCP) binding for the +[Booking Capability](index.md). + +## Protocol Fundamentals + +### Discovery + +Businesses advertise MCP transport availability through their UCP profile at +`/.well-known/ucp`. + + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "services": { + "dev.ucp.lodging": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", + "transport": "mcp", + "schema": "https://ucp.dev/{{ ucp_version }}/services/lodging/mcp.openrpc.json", + "endpoint": "https://business.example.com/ucp/mcp" + } + ] + }, + "capabilities": { + "dev.ucp.lodging.booking": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/lodging/booking", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/lodging/booking.json" + } + ] + }, + "payment_handlers": { + "com.example.vendor.delegate_payment": [ + { + "id": "handler_1", + "version": "{{ ucp_version }}", + "spec": "https://example.vendor.com/specs/delegate-payment", + "schema": "https://example.vendor.com/schemas/delegate-payment-config.json", + "available_instruments": [ + {"type": "card", "constraints": {"properties": {"brand": {"enum": ["visa", "mastercard"]}}}} + ], + "config": {...} + } + ] + } + } +} +``` + +### Request Metadata + +MCP clients **MUST** include a `meta` object in every request containing +protocol metadata: + + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "start_booking_session", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/lodging-agent.json" + }, + "idempotency-key": "550e8400-e29b-41d4-a716-446655440000" + }, + "booking": { + "accommodation": {...}, + "room_rates": [...], + "itinerary": {...} + } + } + } +} +``` + +The `meta["ucp-agent"]` field is **required** on all requests to enable +[capability negotiation](../../overview/index.md#negotiation-protocol). The +`complete_booking_session` operation also requires +`meta["idempotency-key"]` for retry safety. Platforms **MAY** include +additional metadata fields. + +## Tools + +UCP Capabilities map 1:1 to MCP Tools. + +### Identifier Pattern + +MCP tools separate resource identification from payload data: + +* **Requests:** For operations on existing booking sessions (`get`, `update`, + `complete`, `cancel`), a top-level `id` parameter identifies the target + resource. The `booking` object in the request payload **MUST NOT** contain + an `id` field. +* **Responses:** All responses **MUST** include `booking.id` as part of the full resource state. +* **Create:** The `create_booking_session` operation does not require an `id` in the request, and the response includes the newly assigned `booking.id`. + +| Tool | Operation | Description | +| :------------------------- | :------------------------------------------------------------------ | :------------------------- | +| `create_booking_session` | [Create Booking Session](index.md#create-booking-session) | Create a booking session. | +| `get_booking_session` | [Get Booking Session](index.md#get-booking-session) | Get a booking session. | +| `update_booking_session` | [Update Booking Session](index.md#update-booking-session) | Update a booking session. | +| `complete_booking_session` | [Complete Booking Session](index.md#complete-booking-session) | Complete booking. | +| `cancel_booking_session` | [Cancel Booking Session](index.md#cancel-booking-session) | Cancel a booking session. | + +### `create_booking_session` + +Maps to the [Create Booking Session](index.md#create-booking-session) operation. + +#### Input Schema + +{{ schema_fields('booking_create_req', 'lodging/booking/mcp') }} + +#### Output Schema + +{{ schema_fields('booking_resp', 'lodging/booking/mcp') }} + +#### Example + +=== "Request" + + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "create_booking_session", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-07/lodging-agent.json" + } + }, + "booking": { + "accommodation": { + "id": "hotel_123" + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen" + }, + "rate_plan": { + "id": "rp_avg_base_rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + } + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + } + } + } + } + } + ``` + +=== "Response" + + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "structuredContent": { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.lodging.booking": [ + {"version": "{{ ucp_version }}"} + ] + }, + "payment_handlers": { + "com.example.vendor.delegate_payment": [ + {"id": "handler_1", "version": "{{ ucp_version }}", "available_instruments": [{"type": "card"}], "config": {}} + ] + } + }, + "id": "booking_123", + "status": "ready_for_complete", + "accommodation": { + "id": "hotel_123", + "name": "Beautiful Scenery Hotel", + "address": { + "street_address": "123 Scene St", + "address_locality": "Phoenix", + "address_region": "AZ", + "address_country": "US", + "postal_code": "85004" + } + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen", + "title": "Luxury Queen Room with Two Queen Beds", + "capacity": { + "adults": 2, + "children": [ + { + "from_age": 0, + "to_age": 5, + "total": 1 + }, + { + "from_age": 6, + "to_age": 16, + "total": 1 + } + ], + "total": 4 + } + }, + "rate_plan": { + "id": "rp_avg_base_rate", + "title": "Best Available Rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + }, + "totals": [ + { + "type": "subtotal", + "amount": 55000 + }, + { + "type": "tax", + "amount": 5500 + }, + { + "type": "total", + "amount": 60500 + } + ] + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + }, + "currency": "USD", + "totals": [ + { + "type": "subtotal", + "amount": 385000 + }, + { + "type": "tax", + "amount": 38500 + }, + { + "type": "fee", + "display_text": "Booking fee", + "amount": 1000 + }, + { + "type": "total", + "amount": 424500 + } + ], + "links": [ + { + "type": "privacy_policy", + "url": "https://business.example.com/privacy" + }, + { + "type": "terms_of_service", + "url": "https://business.example.com/terms" + }, + { + "type": "cancellation_policy", + "url": "https://business.example.com/cancellation" + } + ], + "continue_url": "https://business.example.com/booking-sessions/booking_123", + "expires_at": "2026-06-01T18:30:00Z" + }, + "content": [ + { + "type": "text", + "text": "{\"ucp\":{…},…}" + } + ] + } + } + ``` + +=== "Error Response" + + Selected room is no longer available — no booking session resource is created: + + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "structuredContent": { + "ucp": { + "version": "{{ ucp_version }}", + "status": "error" + }, + "messages": [ + { + "type": "error", + "code": "inventory_exhausted", + "content": "Selected room is no longer available.", + "severity": "unrecoverable" + } + ], + "continue_url": "https://business.com/" + }, + "content": [ + {"type": "text", "text": "..."} + ] + } + } + ``` + +### `get_booking_session` + +Maps to the [Get Booking Session](index.md#get-booking-session) operation. + +#### Input Schema + +* `id` (String): **Required**. The ID of the booking session to retrieve. + +#### Output Schema + +{{ schema_fields('booking_resp', 'lodging/booking/mcp') }} + +### `update_checkout` + +Maps to the [Update Booking](index.md#update-booking-session) operation. + +#### Input Schema + +* `id` (String): **Required**. The ID of the booking session to update. + +{{ schema_fields('booking_update_req', 'lodging/booking/mcp') }} + +#### Output Schema + +{{ schema_fields('booking_resp', 'lodging/booking/mcp') }} + +#### Example + +=== "Request" + + + ```json + { + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "update_booking_session", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-07/lodging-agent.json" + } + }, + "id": "booking_123", + "booking": { + "accommodation": { + "id": "hotel_123" + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen" + }, + "rate_plan": { + "id": "rp_avg_base_rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + }, + "guest_assignments": [ + { + "guest_id": "gst_01", + "role": "primary_guest" + }, + { + "guest_id": "gst_02", + "role": "additional_guest" + } + ] + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + }, + "guests": [ + { + "id": "gst_01", + "first_name": "Jane", + "last_name": "Doe", + "email": "jane.doe@example.com", + "phone_number": "+14155551234" + }, + { + "id": "gst_02", + "first_name": "Mary", + "last_name": "Doe" + } + ] + } + } + } + } + ``` + +=== "Response" + + + ```json + { + "jsonrpc": "2.0", + "id": 3, + "result": { + "structuredContent": { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.lodging.booking": [ + {"version": "{{ ucp_version }}"} + ] + }, + "payment_handlers": { + "com.example.vendor.delegate_payment": [ + {"id": "handler_1", "version": "{{ ucp_version }}", "available_instruments": [{"type": "card"}], "config": {}} + ] + } + }, + "id": "booking_123", + "status": "ready_for_complete", + "accommodation": { + "id": "hotel_123", + "name": "Beautiful Scenery Hotel", + "address": { + "street_address": "123 Scene St", + "address_locality": "Phoenix", + "address_region": "AZ", + "address_country": "US", + "postal_code": "85004" + } + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen", + "title": "Luxury Queen Room with Two Queen Beds", + "capacity": { + "adults": 2, + "children": [ + { + "from_age": 0, + "to_age": 5, + "total": 1 + }, + { + "from_age": 6, + "to_age": 16, + "total": 1 + } + ], + "total": 4 + } + }, + "rate_plan": { + "id": "rp_avg_base_rate", + "title": "Best Available Rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + }, + "guest_assignments": [ + { + "guest_id": "gst_01", + "role": "primary_guest" + }, + { + "guest_id": "gst_02", + "role": "additional_guest" + } + ], + "totals": [ + { + "type": "subtotal", + "amount": 55000 + }, + { + "type": "tax", + "amount": 5500 + }, + { + "type": "total", + "amount": 60500 + } + ] + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + }, + "guests": [ + { + "id": "gst_01", + "first_name": "Jane", + "last_name": "Doe", + "email": "jane.doe@example.com", + "phone_number": "+14155551234" + }, + { + "id": "gst_02", + "first_name": "Mary", + "last_name": "Doe" + } + ], + "currency": "USD", + "totals": [ + { + "type": "subtotal", + "amount": 385000 + }, + { + "type": "tax", + "amount": 38500 + }, + { + "type": "fee", + "display_text": "Booking fee", + "amount": 1000 + }, + { + "type": "total", + "amount": 424500 + } + ], + "links": [ + { + "type": "privacy_policy", + "url": "https://business.example.com/privacy" + }, + { + "type": "terms_of_service", + "url": "https://business.example.com/terms" + }, + { + "type": "cancellation_policy", + "url": "https://business.example.com/cancellation" + } + ], + "continue_url": "https://business.example.com/booking-sessions/booking_123", + "expires_at": "2026-06-01T18:30:00Z" + }, + "content": [ + { + "type": "text", + "text": "{\"ucp\":{…},…}" + } + ] + } + } + ``` + +### `complete_booking_session` + +Maps to the [Complete Booking Session](index.md#complete-booking-session) operation. + +#### Input Schema + +* `id` (String): **Required**. The ID of the checkout session. + +{{ schema_fields('booking_complete_req', 'lodging/booking/mcp') }} + +#### Output Schema + +{{ schema_fields('booking_resp', 'lodging/booking/mcp') }} + +**Note:** Response **MUST** include a `confirmation` object if completion succeeds. + +#### Example + +=== "Request" + + + ```json + { + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "complete_booking_session", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-07/lodging-agent.json" + }, + "idempotency-key": "550e8400-e29b-41d4-a716-446655440000" + }, + "id": "booking_123", + "booking": { + "payment": { + "instruments": [ + { + "id": "pi_handler_1", + "handler_id": "handler_1", + "type": "card", + "selected": true, + "display": { + "brand": "mastercard", + "last_digits": "5678", + "card_art": "https://cart-art-1.html", + "description": "Vendor Pay •••• 5678" + }, + "billing_address": { + "street_address": "123 Main St", + "address_locality": "Mountain View", + "address_region": "CA", + "address_country": "US", + "postal_code": "94040" + }, + "credential": { + "type": "PAYMENT_GATEWAY", + "token": "examplePaymentMethodToken" + } + } + ] + }, + "signals": { + "dev.ucp.user_agent": "Mozilla/5.0 ..." + } + } + } + } + } + ``` + +=== "Response" + + + ```json + { + "jsonrpc": "2.0", + "id": 2, + "result": { + "structuredContent": { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.lodging.booking": [ + {"version": "{{ ucp_version }}"} + ] + }, + "payment_handlers": { + "com.example.vendor.delegate_payment": [ + {"id": "handler_1", "version": "{{ ucp_version }}", "available_instruments": [{"type": "card"}], "config": {}} + ] + } + }, + "id": "booking_123", + "status": "completed", + "accommodation": { + "id": "hotel_123", + "name": "Beautiful Scenery Hotel", + "address": { + "street_address": "123 Scene St", + "address_locality": "Phoenix", + "address_region": "AZ", + "address_country": "US", + "postal_code": "85004" + } + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen", + "title": "Luxury Queen Room with Two Queen Beds", + "capacity": { + "adults": 2, + "children": [ + { + "from_age": 0, + "to_age": 5, + "total": 1 + }, + { + "from_age": 6, + "to_age": 16, + "total": 1 + } + ], + "total": 4 + } + }, + "rate_plan": { + "id": "rp_avg_base_rate", + "title": "Best Available Rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + }, + "guest_assignments": [ + { + "guest_id": "gst_01", + "role": "primary_guest" + }, + { + "guest_id": "gst_02", + "role": "additional_guest" + } + ], + "totals": [ + { + "type": "subtotal", + "amount": 55000 + }, + { + "type": "tax", + "amount": 5500 + }, + { + "type": "total", + "amount": 60500 + } + ] + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + }, + "guests": [ + { + "id": "gst_01", + "first_name": "Jane", + "last_name": "Doe", + "email": "jane.doe@example.com", + "phone_number": "+14155551234" + }, + { + "id": "gst_02", + "first_name": "Mary", + "last_name": "Doe" + } + ], + "currency": "USD", + "totals": [ + { + "type": "subtotal", + "amount": 385000 + }, + { + "type": "tax", + "amount": 38500 + }, + { + "type": "fee", + "display_text": "Booking fee", + "amount": 1000 + }, + { + "type": "total", + "amount": 424500 + } + ], + "links": [ + { + "type": "privacy_policy", + "url": "https://business.example.com/privacy" + }, + { + "type": "terms_of_service", + "url": "https://business.example.com/terms" + }, + { + "type": "cancellation_policy", + "url": "https://business.example.com/cancellation" + } + ], + "confirmation": { + "id": "confirmation_123", + "label": "CON123AZ", + "pincode": "1234" + }, + "payment": {...}, + "signals": {...} + }, + "content": [ + { + "type": "text", + "text": "{\"ucp\":{…},…}" + } + ] + } + } + ``` + +### `cancel_booking_session` + +Maps to the [Cancel Booking Session](index.md#cancel-booking-session) operation. + +#### Input Schema + +* `id` (String): **Required**. The ID of the booking session. + +#### Output Schema + +{{ schema_fields('booking_resp', 'lodging/booking/mcp') }} + +**Note:** Response **MUST** include `"status": "canceled"` if cancellation succeeds. + +## Error Handling + +UCP distinguishes between protocol errors and business outcomes. See the +[Core Specification](../../overview/index.md#error-handling) for the complete error code +registry and transport binding examples. + +* **Protocol errors**: Transport-level failures (authentication, rate limiting, + unavailability) that prevent request processing. Returned as JSON-RPC + `error` with code `-32000` (or `-32001` for discovery errors). +* **Business outcomes**: Application-level results from successful request + processing, returned as JSON-RPC `result` with UCP envelope and `messages`. + +### Business Outcomes + +Business outcomes (including errors like unavailable merchandise) are returned +as JSON-RPC `result` with `structuredContent` containing the UCP envelope and +`messages`: + + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "structuredContent": { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.lodging.booking": [ + {"version": "{{ ucp_version }}"} + ] + }, + "payment_handlers": { + "com.example.vendor.delegate_payment": [ + {"id": "handler_1", "version": "{{ ucp_version }}", "available_instruments": [{"type": "card"}], "config": {}} + ] + } + }, + "id": "booking_123", + "status": "ready_for_complete", + "accommodation": { + "id": "hotel_123", + "name": "Beautiful Scenery Hotel" + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen", + "title": "Luxury Queen Room with Two Queen Beds", + "capacity": { + "adults": 2, + "children": [ + { + "from_age": 0, + "to_age": 5, + "total": 1 + }, + { + "from_age": 6, + "to_age": 16, + "total": 1 + } + ], + "total": 4 + } + }, + "rate_plan": { + "id": "rp_avg_base_rate", + "title": "Best Available Rate" + }, + "occupancy": { + "adults": 6, + "total": 6 + } + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + }, + "currency": "USD", + "totals": [ + { + "type": "subtotal", + "amount": 350000 + }, + { + "type": "tax", + "amount": 42000 + }, + { + "type": "fee", + "amount": 32500 + }, + { + "type": "total", + "amount": 424500 + } + ], + "links": [], + "continue_url": "https://business.example.com/booking-sessions/booking_123", + "expires_at": "2026-06-01T18:30:00Z", + "messages": [ + { + "type": "error", + "code": "occupancy_exceeded_capacity", + "content": "Number of additional guests requested surpassed room capacity.", + "path": "$.room_rates[0]", + "severity": "recoverable" + } + ] + }, + "content": [ + {"type": "text", "text": "{\"ucp\":{…},…}"} + ] + } +} +``` + +For `create_booking_session`, when no booking session can be created, +JSON-RPC `result` with `structuredContent` containing the UCP envelope and `messages`: + + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "structuredContent": { + "ucp": { "version": "{{ ucp_version }}", "status": "error" }, + "messages": [ + { + "type": "error", + "code": "inventory_exhausted", + "content": "Selected room is no longer available.", + "severity": "unrecoverable" + } + ], + "continue_url": "https://business.com/" + }, + "content": [ + {"type": "text", "text": "{\"ucp\":{…},…}"} + ] + } +} +``` + +## Message Signing + +Platforms **SHOULD** authenticate agents when using MCP transport. When using +HTTP Message Signatures, all checkout operations follow the +[Message Signatures](../../signatures.md) specification. + +### Request Signing + +UCP's MCP transport uses **streamable HTTP**, allowing the same RFC 9421 +signature mechanism as REST. The signature is applied at the HTTP layer: + +| Header | Required | Description | +| :----------------------- | :------- | :--------------------------------------- | +| `Signature-Input` | Yes | Describes signed components | +| `Signature` | Yes | Contains the signature value | +| `Content-Digest` | Yes | SHA-256 hash of request body | +| `UCP-Agent` | Yes | Signer identity (profile URL) | +| `Idempotency-Key` | Yes | Unique key for replay protection | + +**Example Signed Request:** + +```http +POST /mcp HTTP/1.1 +Host: business.example.com +Content-Type: application/json +UCP-Agent: profile="https://platform.example/.well-known/ucp" +Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 +Content-Digest: sha-256=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=: +Signature-Input: sig1=("@method" "@authority" "@path" "content-digest" "content-type" "ucp-agent" "idempotency-key");keyid="platform-2026" +Signature: sig1=:MEUCIQDXyK9N3p5Rt...: + +{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"complete_booking_session","arguments":{"id":"booking_123","booking":{"payment":{...}, ...}}}} +``` + +The `Content-Digest` binds the JSON-RPC body to the signature. No JSON +canonicalization is required. + +See [Message Signatures - MCP Transport](../../signatures.md#mcp-transport) +for details. + +### Response Signing + +Response signatures are **RECOMMENDED** for: + +* `complete_booking_session` responses (booking confirmation) + +Response signatures are **OPTIONAL** for: + +* `create_booking_session`, `get_booking_session`, `update_booking_session`, `cancel_booking_session` + +**Example Signed Response:** + +```http +HTTP/1.1 200 OK +Content-Type: application/json +Content-Digest: sha-256=:Y5fK8nLmPqRsT3vWxYzAbCdEfGhIjKlMnO...: +Signature-Input: sig1=("@status" "content-digest" "content-type");keyid="business-2026" +Signature: sig1=:MFQCIH7kL9nM2oP5qR8sT1uV4wX6yZaB3cD...: + +{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"..."}],"structuredContent":{"id":"booking_123","status":"completed", ...}}} +``` + +See [Message Signatures - REST Response Signing](../../signatures.md#rest-response-signing) +for the signing algorithm (identical for MCP over HTTP). + +## Conformance + +A conforming MCP transport implementation **MUST**: + +1. Implement JSON-RPC 2.0 protocol correctly. +2. Provide all core checkout tools defined in this specification. +3. Return errors per the [Core Specification](../../overview/index.md#error-handling). +4. Return business outcomes as JSON-RPC `result` with UCP envelope and + `messages` array. +5. Validate tool inputs against UCP schemas. +6. Support HTTP transport with streaming. + +A conforming implementation **SHOULD**: + +1. Authenticate agents using one of the supported mechanisms (API keys, OAuth, + mTLS, or HTTP Message Signatures per [Message Signatures](../../signatures.md)). +2. Verify authentication on incoming requests before processing. + +## Implementation + +UCP operations are defined using [OpenRPC](https://open-rpc.org/) (JSON-RPC +schema format). The [MCP specification](https://modelcontextprotocol.io/) +requires all tool invocations to use a `tools/call` method with the operation +name and arguments wrapped in `params`. Implementers **MUST** apply this +transformation: + +| OpenRPC | MCP | +|:---------|:-------------------| +| `method` | `params.name` | +| `params` | `params.arguments` | + +**Param conventions:** + +* `meta` contains request metadata +* `id` identifies the target resource (path parameter equivalent) +* `booking` contains the domain payload (body equivalent) diff --git a/docs/specification/lodging/booking/rest.md b/docs/specification/lodging/booking/rest.md new file mode 100644 index 000000000..22fa1b8b3 --- /dev/null +++ b/docs/specification/lodging/booking/rest.md @@ -0,0 +1,1362 @@ + + +# Booking Capability - REST Binding + +This document specifies the REST binding for the +[Booking Capability](index.md). + +## Protocol Fundamentals + +### Discovery + +Businesses advertise REST transport availability through their UCP profile at +`/.well-known/ucp`. + + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "services": { + "dev.ucp.lodging": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", + "transport": "rest", + "schema": "https://ucp.dev/{{ ucp_version }}/services/lodging/rest.openapi.json", + "endpoint": "https://business.example.com/ucp/v1" + } + ] + }, + "capabilities": { + "dev.ucp.lodging.booking": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/lodging/booking", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/lodging/booking.json" + } + ] + }, + "payment_handlers": { + "com.example.vendor.delegate_payment": [ + { + "id": "handler_1", + "version": "{{ ucp_version }}", + "spec": "https://example.vendor.com/specs/delegate-payment", + "schema": "https://example.vendor.com/schemas/delegate-payment-config.json", + "available_instruments": [ + {"type": "card", "constraints": {"properties": {"brand": {"enum": ["visa", "mastercard"]}}}} + ], + "config": {...} + } + ] + } + } +} +``` + +### Base URL + +All UCP REST endpoints are relative to the business's base URL, which is +discovered through the UCP profile at `/.well-known/ucp`. The endpoint for the +booking capability is defined in the `rest.endpoint` field of the +business profile. + +### Content Types + +* **Request**: `application/json` +* **Response**: `application/json` + +All request and response bodies **MUST** be valid JSON as specified in +[RFC 8259](https://tools.ietf.org/html/rfc8259){ target="_blank" }. + +### Transport Security + +All REST endpoints **MUST** be served over HTTPS with minimum TLS version +1.3. + +## Operations + +| Operation | Method | Endpoint | Description | +| :------------------------------------------------------------- | :----- | :--------------------------------- | :------------------------- | +| [Create Booking Session](index.md#create-booking-session) | `POST` | `/booking-sessions` | Create a booking session. | +| [Get Booking Session](index.md#get-booking-session) | `GET` | `/booking-sessions/{id}` | Get a booking session. | +| [Update Booking Session](index.md#update-booking-session) | `PUT` | `/booking-sessions/{id}` | Update a booking session. | +| [Complete Booking Session](index.md#complete-booking-session) | `POST` | `/booking-sessions/{id}/complete` | Complete booking. | +| [Cancel Booking Session](index.md#cancel-booking-session) | `POST` | `/booking-sessions/{id}/cancel` | Cancel a booking session. | + +## Examples + +### Create Booking Session + +=== "Request" + + + ```json + POST /booking-sessions HTTP/1.1 + UCP-Agent: profile="https://platform.example/profile" + Content-Type: application/json + ...other required headers... + + { + "accommodation": { + "id": "hotel_123" + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen" + }, + "rate_plan": { + "id": "rp_avg_base_rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + } + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + } + } + ``` + +=== "Response" + + + ```json + HTTP/1.1 201 Created + Content-Type: application/json + + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.lodging.booking": [ + {"version": "{{ ucp_version }}"} + ] + }, + "payment_handlers": { + "com.example.vendor.delegate_payment": [ + {"id": "handler_1", "version": "{{ ucp_version }}", "available_instruments": [{"type": "card"}], "config": {}} + ] + } + }, + "id": "booking_123", + "status": "ready_for_complete", + "accommodation": { + "id": "hotel_123", + "name": "Beautiful Scenery Hotel", + "address": { + "street_address": "123 Scene St", + "address_locality": "Phoenix", + "address_region": "AZ", + "address_country": "US", + "postal_code": "85004" + } + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen", + "title": "Luxury Queen Room with Two Queen Beds", + "capacity": { + "adults": 2, + "children": [ + { + "from_age": 0, + "to_age": 5, + "total": 1 + }, + { + "from_age": 6, + "to_age": 16, + "total": 1 + } + ], + "total": 4 + } + }, + "rate_plan": { + "id": "rp_avg_base_rate", + "title": "Best Available Rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + }, + "totals": [ + { + "type": "subtotal", + "amount": 55000 + }, + { + "type": "tax", + "amount": 5500 + }, + { + "type": "total", + "amount": 60500 + } + ] + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + }, + "currency": "USD", + "totals": [ + { + "type": "subtotal", + "amount": 385000 + }, + { + "type": "tax", + "amount": 38500 + }, + { + "type": "fee", + "display_text": "Booking fee", + "amount": 1000 + }, + { + "type": "total", + "amount": 424500 + } + ], + "links": [ + { + "type": "privacy_policy", + "url": "https://business.example.com/privacy" + }, + { + "type": "terms_of_service", + "url": "https://business.example.com/terms" + }, + { + "type": "cancellation_policy", + "url": "https://business.example.com/cancellation" + } + ], + "continue_url": "https://business.example.com/booking-sessions/booking_123", + "expires_at": "2026-06-01T18:30:00Z" + } + ``` + +=== "Error Response" + + Selected room is no longer available — no booking session resource is created: + + + ```json + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ucp": { + "version": "{{ ucp_version }}", + "status": "error" + }, + "messages": [ + { + "type": "error", + "code": "inventory_exhausted", + "content": "Selected room is no longer available.", + "severity": "unrecoverable" + } + ], + "continue_url": "https://business.com/" + } + ``` + +### Update Booking Session + +Update calls allow clients to progressively build optional fields +(e.g., `guests`, `booker`, `travel_purpose`) across multiple calls. +Each PUT replaces the entire session, +so clients must include all previously set fields they wish to retain. + +If businesses have specific logic to enforce field existence in `guest`, +`booker`, or addresses (i.e. `billing_address`), this is the right +place to set these expectations via `messages`. + +=== "Request" + + + ```json + PUT /booking-sessions/{id} HTTP/1.1 + UCP-Agent: profile="https://platform.example/profile" + Content-Type: application/json + ...other required headers... + + { + "accommodation": { + "id": "hotel_123" + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen" + }, + "rate_plan": { + "id": "rp_avg_base_rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + }, + "guest_assignments": [ + { + "guest_id": "gst_01", + "role": "primary_guest" + }, + { + "guest_id": "gst_02", + "role": "additional_guest" + } + ] + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + }, + // New data introduced in the update call. + "guests": [ + { + "id": "gst_01", + "first_name": "Jane", + "last_name": "Doe", + "email": "jane.doe@example.com", + "phone_number": "+14155551234" + }, + { + "id": "gst_02", + "first_name": "Mary", + "last_name": "Doe" + } + ], + "booker": { + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "phone_number": "+14155559876", + "birthdate": "1980-08-26", + "address": { + "street_address": "1600 Amphitheatre Pkwy", + "address_locality": "Mountain View", + "address_region": "CA", + "address_country": "US", + "postal_code": "94043" + } + }, + "travel_purpose": "leisure" + } + ``` + +=== "Response" + + + ```json + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.lodging.booking": [ + {"version": "{{ ucp_version }}"} + ] + }, + "payment_handlers": { + "com.example.vendor.delegate_payment": [ + {"id": "handler_1", "version": "{{ ucp_version }}", "available_instruments": [{"type": "card"}], "config": {}} + ] + } + }, + "id": "booking_123", + "status": "ready_for_complete", + "accommodation": { + "id": "hotel_123", + "name": "Beautiful Scenery Hotel", + "address": { + "street_address": "123 Scene St", + "address_locality": "Phoenix", + "address_region": "AZ", + "address_country": "US", + "postal_code": "85004" + } + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen", + "title": "Luxury Queen Room with Two Queen Beds", + "capacity": { + "adults": 2, + "children": [ + { + "from_age": 0, + "to_age": 5, + "total": 1 + }, + { + "from_age": 6, + "to_age": 16, + "total": 1 + } + ], + "total": 4 + } + }, + "rate_plan": { + "id": "rp_avg_base_rate", + "title": "Best Available Rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + }, + "guest_assignments": [ + { + "guest_id": "gst_01", + "role": "primary_guest" + }, + { + "guest_id": "gst_02", + "role": "additional_guest" + } + ], + "totals": [ + { + "type": "subtotal", + "amount": 55000 + }, + { + "type": "tax", + "amount": 5500 + }, + { + "type": "total", + "amount": 60500 + } + ] + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + }, + "guests": [ + { + "id": "gst_01", + "first_name": "Jane", + "last_name": "Doe", + "email": "jane.doe@example.com", + "phone_number": "+14155551234" + }, + { + "id": "gst_02", + "first_name": "Mary", + "last_name": "Doe" + } + ], + "booker": { + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "phone_number": "+14155559876", + "birthdate": "1980-08-26", + "address": { + "street_address": "1600 Amphitheatre Pkwy", + "address_locality": "Mountain View", + "address_region": "CA", + "address_country": "US", + "postal_code": "94043" + } + }, + "travel_purpose": "leisure", + "currency": "USD", + "totals": [ + { + "type": "subtotal", + "amount": 385000 + }, + { + "type": "tax", + "amount": 38500 + }, + { + "type": "fee", + "display_text": "Booking fee", + "amount": 1000 + }, + { + "type": "total", + "amount": 424500 + } + ], + "links": [ + { + "type": "privacy_policy", + "url": "https://business.example.com/privacy" + }, + { + "type": "terms_of_service", + "url": "https://business.example.com/terms" + }, + { + "type": "cancellation_policy", + "url": "https://business.example.com/cancellation" + } + ], + "continue_url": "https://business.example.com/booking-sessions/booking_123", + "expires_at": "2026-06-01T18:30:00Z" + } + ``` + +### Complete Booking Session + +=== "Request" + + + ```json + POST /booking-sessions/{id}/complete + UCP-Agent: profile="https://platform.example/profile" + Content-Type: application/json + ...other required headers... + + { + "payment": { + "instruments": [ + { + "id": "pi_handler_1", + "handler_id": "handler_1", + "type": "card", + "selected": true, + "display": { + "brand": "mastercard", + "last_digits": "5678", + "card_art": "https://cart-art-1.html", + "description": "Vendor Pay •••• 5678" + }, + "billing_address": { + "street_address": "123 Main St", + "address_locality": "Mountain View", + "address_region": "CA", + "address_country": "US", + "postal_code": "94040" + }, + "credential": { + "type": "PAYMENT_GATEWAY", + "token": "examplePaymentMethodToken" + } + } + ] + }, + "signals": { + "dev.ucp.user_agent": "Mozilla/5.0 ..." + } + } + ``` + +=== "Response" + + + ```json + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.lodging.booking": [ + {"version": "{{ ucp_version }}"} + ] + }, + "payment_handlers": { + "com.example.vendor.delegate_payment": [ + {"id": "handler_1", "version": "{{ ucp_version }}", "available_instruments": [{"type": "card"}], "config": {}} + ] + } + }, + "id": "booking_123", + "status": "completed", + "accommodation": { + "id": "hotel_123", + "name": "Beautiful Scenery Hotel", + "address": { + "street_address": "123 Scene St", + "address_locality": "Phoenix", + "address_region": "AZ", + "address_country": "US", + "postal_code": "85004" + } + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen", + "title": "Luxury Queen Room with Two Queen Beds", + "capacity": { + "adults": 2, + "children": [ + { + "from_age": 0, + "to_age": 5, + "total": 1 + }, + { + "from_age": 6, + "to_age": 16, + "total": 1 + } + ], + "total": 4 + } + }, + "rate_plan": { + "id": "rp_avg_base_rate", + "title": "Best Available Rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + }, + "guest_assignments": [ + { + "guest_id": "gst_01", + "role": "primary_guest" + }, + { + "guest_id": "gst_02", + "role": "additional_guest" + } + ], + "totals": [ + { + "type": "subtotal", + "amount": 55000 + }, + { + "type": "tax", + "amount": 5500 + }, + { + "type": "total", + "amount": 60500 + } + ] + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + }, + "guests": [ + { + "id": "gst_01", + "first_name": "Jane", + "last_name": "Doe", + "email": "jane.doe@example.com", + "phone_number": "+14155551234" + }, + { + "id": "gst_02", + "first_name": "Mary", + "last_name": "Doe" + } + ], + "booker": { + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "phone_number": "+14155559876", + "birthdate": "1980-08-26", + "address": { + "street_address": "1600 Amphitheatre Pkwy", + "address_locality": "Mountain View", + "address_region": "CA", + "address_country": "US", + "postal_code": "94043" + } + }, + "travel_purpose": "leisure", + "currency": "USD", + "totals": [ + { + "type": "subtotal", + "amount": 385000 + }, + { + "type": "tax", + "amount": 38500 + }, + { + "type": "fee", + "display_text": "Booking fee", + "amount": 1000 + }, + { + "type": "total", + "amount": 424500 + } + ], + "links": [ + { + "type": "privacy_policy", + "url": "https://business.example.com/privacy" + }, + { + "type": "terms_of_service", + "url": "https://business.example.com/terms" + }, + { + "type": "cancellation_policy", + "url": "https://business.example.com/cancellation" + } + ], + // Booking confirmation that MUST be set in the response. + "confirmation": { + "id": "confirmation_123", + "label": "CON123AZ", + "pincode": "1234" + }, + "payment": {...}, + "signals": {...} + } + ``` + +### Get Booking Session + +=== "Request" + + + ```json + GET /booking-sessions/{id} + UCP-Agent: profile="https://platform.example/profile" + Content-Type: application/json + ...other required headers... + + {} + ``` + +=== "Response" + + + ```json + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.lodging.booking": [ + {"version": "{{ ucp_version }}"} + ] + }, + "payment_handlers": { + "com.example.vendor.delegate_payment": [ + {"id": "handler_1", "version": "{{ ucp_version }}", "available_instruments": [{"type": "card"}], "config": {}} + ] + } + }, + "id": "booking_123", + "status": "completed", + "accommodation": { + "id": "hotel_123", + "name": "Beautiful Scenery Hotel", + "address": { + "street_address": "123 Scene St", + "address_locality": "Phoenix", + "address_region": "AZ", + "address_country": "US", + "postal_code": "85004" + } + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen", + "title": "Luxury Queen Room with Two Queen Beds", + "capacity": { + "adults": 2, + "children": [ + { + "from_age": 0, + "to_age": 5, + "total": 1 + }, + { + "from_age": 6, + "to_age": 16, + "total": 1 + } + ], + "total": 4 + } + }, + "rate_plan": { + "id": "rp_avg_base_rate", + "title": "Best Available Rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + }, + "guest_assignments": [ + { + "guest_id": "gst_01", + "role": "primary_guest" + }, + { + "guest_id": "gst_02", + "role": "additional_guest" + } + ], + "totals": [ + { + "type": "subtotal", + "amount": 55000 + }, + { + "type": "tax", + "amount": 5500 + }, + { + "type": "total", + "amount": 60500 + } + ] + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + }, + "guests": [ + { + "id": "gst_01", + "first_name": "Jane", + "last_name": "Doe", + "email": "jane.doe@example.com", + "phone_number": "+14155551234" + }, + { + "id": "gst_02", + "first_name": "Mary", + "last_name": "Doe" + } + ], + "booker": { + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "phone_number": "+14155559876", + "birthdate": "1980-08-26", + "address": { + "street_address": "1600 Amphitheatre Pkwy", + "address_locality": "Mountain View", + "address_region": "CA", + "address_country": "US", + "postal_code": "94043" + } + }, + "travel_purpose": "leisure", + "currency": "USD", + "totals": [ + { + "type": "subtotal", + "amount": 385000 + }, + { + "type": "tax", + "amount": 38500 + }, + { + "type": "fee", + "display_text": "Booking fee", + "amount": 1000 + }, + { + "type": "total", + "amount": 424500 + } + ], + "links": [ + { + "type": "privacy_policy", + "url": "https://business.example.com/privacy" + }, + { + "type": "terms_of_service", + "url": "https://business.example.com/terms" + }, + { + "type": "cancellation_policy", + "url": "https://business.example.com/cancellation" + } + ], + "confirmation": { + "id": "confirmation_123", + "label": "CON123AZ", + "pincode": "1234" + }, + "payment": {...}, + "signals": {...} + } + ``` + +### Cancel Booking Session + +=== "Request" + + + ```json + POST /booking-sessions/{id}/cancel + UCP-Agent: profile="https://platform.example/profile" + Content-Type: application/json + ...other required headers... + + {} + ``` + +=== "Response" + + + ```json + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.lodging.booking": [ + {"version": "{{ ucp_version }}"} + ] + }, + "payment_handlers": { + "com.example.vendor.delegate_payment": [ + {"id": "handler_1", "version": "{{ ucp_version }}", "available_instruments": [{"type": "card"}], "config": {}} + ] + } + }, + "id": "booking_123", + // Status is updated upon a successful cancellation. + "status": "canceled", + "accommodation": { + "id": "hotel_123", + "name": "Beautiful Scenery Hotel", + "address": { + "street_address": "123 Scene St", + "address_locality": "Phoenix", + "address_region": "AZ", + "address_country": "US", + "postal_code": "85004" + } + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen", + "title": "Luxury Queen Room with Two Queen Beds", + "capacity": { + "adults": 2, + "children": [ + { + "from_age": 0, + "to_age": 5, + "total": 1 + }, + { + "from_age": 6, + "to_age": 16, + "total": 1 + } + ], + "total": 4 + } + }, + "rate_plan": { + "id": "rp_avg_base_rate", + "title": "Best Available Rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + }, + "guest_assignments": [ + { + "guest_id": "gst_01", + "role": "primary_guest" + }, + { + "guest_id": "gst_02", + "role": "additional_guest" + } + ], + "totals": [ + { + "type": "subtotal", + "amount": 55000 + }, + { + "type": "tax", + "amount": 5500 + }, + { + "type": "total", + "amount": 60500 + } + ] + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + }, + "guests": [ + { + "id": "gst_01", + "first_name": "Jane", + "last_name": "Doe", + "email": "jane.doe@example.com", + "phone_number": "+14155551234" + }, + { + "id": "gst_02", + "first_name": "Mary", + "last_name": "Doe" + } + ], + "booker": { + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "phone_number": "+14155559876", + "birthdate": "1980-08-26", + "address": { + "street_address": "1600 Amphitheatre Pkwy", + "address_locality": "Mountain View", + "address_region": "CA", + "address_country": "US", + "postal_code": "94043" + } + }, + "travel_purpose": "leisure", + "currency": "USD", + "totals": [ + { + "type": "subtotal", + "amount": 385000 + }, + { + "type": "tax", + "amount": 38500 + }, + { + "type": "fee", + "display_text": "Booking fee", + "amount": 1000 + }, + { + "type": "total", + "amount": 424500 + } + ], + "links": [ + { + "type": "privacy_policy", + "url": "https://business.example.com/privacy" + }, + { + "type": "terms_of_service", + "url": "https://business.example.com/terms" + }, + { + "type": "cancellation_policy", + "url": "https://business.example.com/cancellation" + } + ], + "continue_url": "https://business.example.com/booking-sessions/booking_123" + } + ``` + +## HTTP Headers + +The following headers are defined for the HTTP binding and apply to all +operations unless otherwise noted. + +{{ header_fields('create_booking_session', 'lodging/rest.openapi.json') }} + +### Specific Header Requirements + +* **UCP-Agent**: All requests **MUST** include the `UCP-Agent` header + containing the platform profile URI using Dictionary Structured Field syntax + ([RFC 8941](https://datatracker.ietf.org/doc/html/rfc8941){target="_blank"}). + Format: `profile="https://platform.example/profile"`. +* **Idempotency-Key**: Operations that modify state **SHOULD** support + idempotency. When provided, the server **MUST**: + 1. Store the key with the operation result for at least 24 hours. + 2. Return the cached result for duplicate keys. + 3. Return `409 Conflict` if the key is reused with different parameters. + +## Protocol Mechanics + +### Status Codes + +UCP uses standard HTTP status codes to indicate the success or failure of an API +request. + +| Status Code | Description | +| :-------------------------- | :--------------------------------------------------------------------------------- | +| `200 OK` | The request was successful. | +| `201 Created` | The resource was successfully created. | +| `400 Bad Request` | The request was invalid or cannot be served. | +| `401 Unauthorized` | Authentication is required and has failed or has not been provided. | +| `403 Forbidden` | The request is authenticated but the user does not have the necessary permissions. | +| `409 Conflict` | The request could not be completed due to a conflict (e.g., idempotent key reuse). | +| `422 Unprocessable Entity` | The profile content is malformed (discovery failure). | +| `424 Failed Dependency` | The profile URL is valid but fetch failed (discovery failure). | +| `429 Too Many Requests` | Rate limit exceeded. | +| `503 Service Unavailable` | Temporary unavailability. | +| `500 Internal Server Error` | An unexpected condition was encountered on the server. | + +### Error Responses + +See the [Core Specification](../../overview/index.md#error-handling) for the complete error +code registry and transport binding examples. + +* **Protocol errors**: Return appropriate HTTP status code (401, 403, 409, 429, + 503) with JSON body containing `code` and `content`. +* **Business outcomes**: Return HTTP 200 with UCP envelope and `messages` array. + +#### Business Outcomes + +Business outcomes (including errors like invalid guest info) are returned +with HTTP 200 and the UCP envelope containing `messages`: + + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.lodging.booking": [ + {"version": "{{ ucp_version }}"} + ] + }, + "payment_handlers": { + "com.example.vendor.delegate_payment": [ + {"id": "handler_1", "version": "{{ ucp_version }}", "available_instruments": [{"type": "card"}], "config": {}} + ] + } + }, + "id": "booking_123", + "status": "ready_for_complete", + "accommodation": { + "id": "hotel_123", + "name": "Beautiful Scenery Hotel" + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen", + "title": "Luxury Queen Room with Two Queen Beds", + "capacity": { + "adults": 2, + "children": [ + { + "from_age": 0, + "to_age": 5, + "total": 1 + }, + { + "from_age": 6, + "to_age": 16, + "total": 1 + } + ], + "total": 4 + } + }, + "rate_plan": { + "id": "rp_avg_base_rate", + "title": "Best Available Rate" + }, + "occupancy": { + "adults": 6, + "total": 6 + } + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + }, + "currency": "USD", + "totals": [ + { + "type": "subtotal", + "amount": 350000 + }, + { + "type": "tax", + "amount": 42000 + }, + { + "type": "fee", + "amount": 32500 + }, + { + "type": "total", + "amount": 424500 + } + ], + "links": [], + "continue_url": "https://business.example.com/booking-sessions/booking_123", + "expires_at": "2026-06-01T18:30:00Z", + "messages": [ + { + "type": "error", + "code": "occupancy_exceeded_capacity", + "content": "Number of additional guests requested surpassed room capacity.", + "path": "$.room_rates[0]", + "severity": "recoverable" + } + ] +} +``` + +For `create_booking_session`, when no booking session can be created, +business **MUST** return HTTP 200 and the UCP envelope containing `messages` + + +```json +{ + "ucp": { "version": "{{ ucp_version }}", "status": "error" }, + "messages": [ + { + "type": "error", + "code": "inventory_exhausted", + "content": "Selected room is no longer available.", + "severity": "unrecoverable" + } + ], + "continue_url": "https://business.com/" +} +``` + +## Message Signing + +Platforms **MAY** choose among authentication mechanisms (API keys, OAuth, +mTLS, HTTP Message Signatures). When using +HTTP Message Signatures, booking operations follow the +[Message Signatures](../../signatures.md) specification. + +### Request Signing + +When HTTP Message Signatures are used, requests **MUST** include valid +`Signature-Input` and `Signature` headers (and `Content-Digest` when a body +is present) per RFC 9421: + +| Header | Required | Description | +| :----------------------- | :------- | :--------------------------------------- | +| `Signature-Input` | Yes | Describes signed components | +| `Signature` | Yes | Contains the signature value | +| `Content-Digest` | Cond.* | SHA-256 hash of request body | +| `UCP-Agent` | Yes | Signer identity (profile URL) | +| `Idempotency-Key` | Yes | Unique key for replay protection | + +\* Required for requests with a body (POST, PUT) + +**Example Signed Request:** + +```http +POST /booking-sessions HTTP/1.1 +Host: business.example.com +Content-Type: application/json +UCP-Agent: profile="https://platform.example/.well-known/ucp" +Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 +Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=: +Signature-Input: sig1=("@method" "@authority" "@path" "idempotency-key" "content-digest" "content-type");keyid="platform-2025" +Signature: sig1=:MEUCIQDTxNq8h7LGHpvVZQp1iHkFp9+3N8Mxk2zH1wK4YuVN8w...: + +{"payment":{...}, ...} +``` + +See [Message Signatures - REST Request Signing](../../signatures.md#rest-request-signing) +for the complete signing algorithm. + +### Response Signing + +Response signatures are **RECOMMENDED** for: + +* `complete_booking_session` responses (booking confirmation) + +Response signatures are **OPTIONAL** for: + +* `create_booking_session`, `get_booking_session`, `update_booking_session`, `cancel_booking_session` + +**Example Signed Response:** + +```http +HTTP/1.1 200 OK +Content-Type: application/json +Content-Digest: sha-256=:Y5fK8nLmPqRsT3vWxYzAbCdEfGhIjKlMnO...: +Signature-Input: sig1=("@status" "content-digest" "content-type");keyid="business-2026" +Signature: sig1=:MFQCIH7kL9nM2oP5qR8sT1uV4wX6yZaB3cD...: + +{"id":"booking_123","status":"completed","confirmation":{...}} +``` + +See [Message Signatures - REST Response Signing](../../signatures.md#rest-response-signing) +for the complete signing algorithm. + +## Security Considerations + +### Authentication + +Authentication is optional and depends on business requirements. When +authentication is required, the REST transport **MAY** use: + +1. **Open API**: No authentication required for public operations. +2. **API Keys**: Via `X-API-Key` header. +3. **OAuth 2.0**: Via `Authorization: Bearer {token}` header. Identifies the + platform for agent-authenticated access, or both platform and user for + user-authenticated access (see [Identity Linking](../../common/identity-linking/index.md)). +4. **Mutual TLS**: For high-security environments. +5. **HTTP Message Signatures**: Per [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421) + (see [Message Signing](#message-signing) above). + +Businesses **MAY** require authentication for some operations while leaving +others open (e.g., public booking without authentication). diff --git a/docs/specification/lodging/extensions/cancellation-policy.md b/docs/specification/lodging/extensions/cancellation-policy.md new file mode 100644 index 000000000..d819194d3 --- /dev/null +++ b/docs/specification/lodging/extensions/cancellation-policy.md @@ -0,0 +1,161 @@ + + +# Cancellation Policy Extension + +## Overview + +The Cancellation Policy Extension defines the +`dev.ucp.lodging.policy.cancellation` policy type on the core +[`policies[]`](../../overview/index.md#policies) primitive for the lodging service. It +adds pre-purchase, machine-readable cancellation terms to policies that +carry this type, so platforms can answer questions like "Can I cancel this booking?", +"Is it free cancellation or is there a fee?", and "Is this rate completely +non-refundable?" without leaving to parse external policy pages. + +**Key features:** + +- Tri-state refundability classification (`refundability`) to signal whether + cancellation is currently free (`refundable`), incurs a penalty + (`partially_refundable`), or is disallowed (`non_refundable`) +- Human-readable summary in `description` carrying detailed property cutoff + times, timezone deadlines, and penalty schedules +- Optional direct link to the property's complete legal terms (`url`) + +**Dependencies:** + +- The core `policies[]` primitive (see [Policies](../../overview/index.md#policies)). +- Any parent capabilities this type extends: Booking + (`dev.ucp.lodging.booking`). + +## Discovery + +Businesses advertise cancellation policy support in their profile. The type +extends any surface that carries `policies[]`: + + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.lodging.policy.cancellation": [ + { + "version": "{{ ucp_version }}", + "extends": [ + "dev.ucp.lodging.booking" + ], + "spec": "https://ucp.dev/{{ ucp_version }}/specification/lodging/extensions/cancellation-policy", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/lodging/policy_cancellation.json" + } + ] + } + } +} +``` + +## Schema + +When this type is active, a `policies[]` entry whose `type` is +`dev.ucp.lodging.policy.cancellation` carries additional attributes +(e.g., `refundability`) in addition to the base `type`, +`description`, `applies_to`, and `url`. + +{{ extension_schema_fields('policy_cancellation.json#/$defs/cancellation_item', 'lodging/extensions/cancellation-policy') }} + +## Cancellation terms + +### Refundability classifications + +The `refundability` field provides a standardized high-level classification: + +- **`refundable`**: Free cancellation is currently available. The booker can + cancel without penalty before the deadline stated in `description`. +- **`partially_refundable`**: The booking can be cancelled, but a cancellation + fee applies (e.g., a one-night room charge or fixed administrative fee), or + the booking is currently inside a partial penalty window. Specific cancellation + fees, penalty schedules, and refund effects are outlined in the policy's + `description` field. +- **`non_refundable`**: The reservation cannot be refunded upon cancellation + (the full booking price is retained by the business, subject to local regulation). + +### Non-refundable bookings + +To signal that a booking or rate is non-refundable, a business **MUST** set +`refundability` to `"non_refundable"`. + +When a business requires the booker to be shown that a booking is non-refundable +prior to confirmation, it emits a `messages[]` warning with +`presentation: "disclosure"` and `code` equal to +`dev.ucp.lodging.policy.cancellation`, targeting the item. The disclosure pairs +with the governing cancellation policy at that node, as defined in +[Presenting policies](../../overview/index.md#presenting-policies). + +### Human-readable descriptions + +Because lodging cancellation rules frequently incorporate specific property +local cutoff times (e.g., "by 3:00 PM property time 2 days before check-in") +and seasonal rules, businesses **MUST** articulate the full timeline and terms in +`description`. The `description` and `refundability` field **MUST NOT** contradict +each other. + +## Targeting and precedence + +Targeting and precedence are provided by the `policies[]` primitive and are not +redefined here. In short: a policy with no `applies_to` is the response-wide +default; a policy that targets specific room overrides will result in the +narrowest same-type target winning. See +[Targeting](../../overview/index.md#targeting) and +[Precedence](../../overview/index.md#precedence). + +For lodging bookings, a common example is when a business states a single default cancellation +policy once, then adds targeted overrides only for specific exceptions (such as +a non-refundable room rate or promotional upgrade). + +## Responsibilities + +Cancellation policies are business-stated facts. They are response-only data +that a platform never submits. They carry no user-asserted claims and no PII. + +A business **SHOULD** accurately summarize cancellation timelines, applicable +penalties and cutoff deadlines in `description` and link to full policy terms +via `url`. A platform **SHOULD** surface `url` alongside the policy description +so the booker can review full dynamic property policies. + +## Examples + + +```json +[ + { + "type": "dev.ucp.lodging.policy.cancellation", + "description": { + "plain": "Free cancellation until Dec 20, 2026, 3:00 PM EDT (48 hours before check-in). 1 night penalty thereafter." + }, + "refundability": "refundable", + "url": "https://example.com/cancellation-terms" + }, + { + "type": "dev.ucp.lodging.policy.cancellation", + "description": { + "plain": "Non-refundable promotional rate. This room reservation cannot be cancelled or modified for a refund." + }, + "applies_to": ["$.room_rates[0]"], + "refundability": "non_refundable", + "url": "https://example.com/cancellation-terms#non-refundable" + } +] +``` diff --git a/docs/specification/reference.md b/docs/specification/reference.md index 6b1fb7147..2630a80f8 100644 --- a/docs/specification/reference.md +++ b/docs/specification/reference.md @@ -25,12 +25,16 @@ within the UCP. {{ auto_generate_schema_reference('.', 'reference', include_extensions=False) }} +{{ auto_generate_schema_reference('.', 'reference', include_extensions=False, base_dir='source/schemas/lodging') }} + ## Type Schemas {{ auto_generate_schema_reference('types', 'reference', include_extensions=False, base_dir='source/schemas/common') }} {{ auto_generate_schema_reference('types', 'reference', include_extensions=False) }} +{{ auto_generate_schema_reference('types', 'reference', include_extensions=False, base_dir='source/schemas/lodging') }} + ### Selected Payment Instrument {: #payment-instrument-selected-payment-instrument } {{ extension_schema_fields('types/payment_instrument.json#/$defs/selected_payment_instrument', 'reference') }} @@ -61,6 +65,8 @@ within the UCP. {{ auto_generate_schema_reference('.', 'reference', include_capability=False) }} +{{ auto_generate_schema_reference('.', 'reference', include_capability=False, base_dir='source/schemas/lodging') }} + ## UCP Metadata The following schemas define the structure of UCP metadata used in discovery @@ -108,6 +114,12 @@ The `ucp` object included in location responses. {{ extension_schema_fields('ucp.json#/$defs/response_location_schema', 'reference') }} +### Booking Response Metadata {: #ucp-response-booking-schema } + +The `ucp` object included in booking responses. + +{{ extension_schema_fields('ucp.json#/$defs/response_booking_schema', 'reference') }} + ### Capability This object describes a single capability or extension. It appears in the diff --git a/main.py b/main.py index 2d5d1309a..47068d512 100644 --- a/main.py +++ b/main.py @@ -377,17 +377,20 @@ def create_link(ref_string, spec_file_name, context=None): if ref_string.startswith("types/"): spec_file_name = "reference" - # Redirect refs to common/types/ or shopping/types/ schemas to reference. + # Redirect refs to common/types/ or /types/ schemas to reference. # Uses ref_path (fragment stripped) so refs like # "../common/types/pagination.json#/$defs/request" are handled correctly. elif ref_path.endswith(".json"): filename_only = Path(ref_path).name common_type_path = COMMON_TYPES_DIR / filename_only - shopping_type_path = SHOPPING_TYPES_DIR / filename_only - shopping_path = SHOPPING_SCHEMAS_DIR / filename_only - if common_type_path.exists() or ( - shopping_type_path.exists() and not shopping_path.exists() - ): + vertical_type_paths = [] + for vertical_dir in VERTICAL_DIRS: + vertical_path = vertical_dir / filename_only + vertical_type_path = vertical_dir / "types" / filename_only + if vertical_type_path.exists() and not vertical_path.exists(): + vertical_type_paths.append(vertical_type_path) + + if common_type_path.exists() or len(vertical_type_paths) > 0: spec_file_name = "reference" filename = Path(ref_path).name @@ -606,7 +609,29 @@ def _render_embedded_table( ) ) - return "\n".join(md) + # When allOf composition overrides a property from an earlier branch, + # prefer the outer (later) definition and render each field only once. + deduped_rows = {} + other_lines = [] + for block in md: + for line in block.splitlines(): + stripped = line.strip() + if not stripped: + continue + if stripped.startswith("|") and stripped.endswith("|"): + parts = [p.strip() for p in stripped.split("|")] + # Exclude header and separator rows if any were embedded + if len(parts) >= 5 and parts[1] not in ("Name", ":---"): + field_name = parts[1] + deduped_rows[field_name] = line + continue + other_lines.append(line) + + result = list(deduped_rows.values()) + if other_lines: + result.extend(other_lines) + + return "\n".join(result) def _field_requirement(field_name, ucp_request, required_list): """Render the Requirement cell for a schema field. @@ -794,10 +819,18 @@ def _deref_self(ref_value): context, ) ) - elif "allOf" in schema_data and not properties: + elif "allOf" in schema_data: + all_of_list = list(schema_data.get("allOf", [])) + if properties: + all_of_list.append( + { + "properties": properties, + "required": schema_data.get("required", []), + } + ) md.append( _render_embedded_table( - schema_data.get("allOf", []), + all_of_list, required_list, spec_file_name, context, diff --git a/mkdocs.yml b/mkdocs.yml index ed8297cb9..589aaa2b0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -86,6 +86,14 @@ nav: - Buyer Consent: specification/shopping/extensions/buyer-consent.md - Playground: specification/shopping/playground.md + - Lodging: + - Booking Capability: + - Overview: specification/lodging/booking/index.md + - Transports: + - REST: specification/lodging/booking/rest.md + - MCP: specification/lodging/booking/mcp.md + - Extensions: + - Cancellation Policy: specification/lodging/extensions/cancellation-policy.md - Payment: - Extensions: - AP2 Mandates Extension: specification/payment/extensions/ap2-mandates.md @@ -526,6 +534,22 @@ plugins: Buyer Consent Extension, defining data schemas for transmitting buyer privacy choices (analytics, marketing, personalization) to assist with privacy compliance (CCPA/GDPR). + Lodging Booking Capability: + - specification/lodging/booking/index.md: >- + Core Booking Capability specification detailing necessary data + exchange and payment processing for finalizing reservations on + a pre-selected room inventory. + - specification/lodging/booking/rest.md: >- + HTTP REST transport binding for Booking operations, including + endpoints for session creation, modification, retrieval, and + cancellation. + - specification/lodging/booking/mcp.md: >- + Model Context Protocol (MCP) transport binding for + Booking operations, mapping reservation session management to + JSON-RPC tools for AI booking agents. + Lodging Extensions: + - specification/lodging/extensions/cancellation-policy.md: >- + TODO: add content Payment: - specification/payment/extensions/ap2-mandates.md: >- AP2 Mandates Extension, detailing secure, cryptographically bound diff --git a/scripts/scaffolds/lodging_booking_request_complete.json b/scripts/scaffolds/lodging_booking_request_complete.json new file mode 100644 index 000000000..ce565f917 --- /dev/null +++ b/scripts/scaffolds/lodging_booking_request_complete.json @@ -0,0 +1,15 @@ +{ + "payment": { + "instruments": [ + { + "id": "instr_scaffold", + "handler_id": "handler_scaffold", + "type": "card", + "credential": { + "type": "token", + "token": "tok_scaffold" + } + } + ] + } +} diff --git a/scripts/scaffolds/lodging_booking_request_create.json b/scripts/scaffolds/lodging_booking_request_create.json new file mode 100644 index 000000000..50dc2baf3 --- /dev/null +++ b/scripts/scaffolds/lodging_booking_request_create.json @@ -0,0 +1,24 @@ +{ + "accommodation": { + "id": "acc_scaffold" + }, + "room_rates": [ + { + "id": "rr_scaffold", + "room_details": { + "id": "room_scaffold" + }, + "rate_plan": { + "id": "rp_scaffold" + }, + "occupancy": { + "adults": 1, + "total": 1 + } + } + ], + "itinerary": { + "start_date": "2026-01-01", + "end_date": "2026-01-05" + } +} diff --git a/scripts/scaffolds/lodging_booking_request_update.json b/scripts/scaffolds/lodging_booking_request_update.json new file mode 100644 index 000000000..a87562ba9 --- /dev/null +++ b/scripts/scaffolds/lodging_booking_request_update.json @@ -0,0 +1,37 @@ +{ + "accommodation": { + "id": "acc_scaffold" + }, + "room_rates": [ + { + "id": "rr_scaffold", + "room_details": { + "id": "room_scaffold" + }, + "rate_plan": { + "id": "rp_scaffold" + }, + "occupancy": { + "adults": 1, + "total": 1 + }, + "guest_assignments": [ + { + "guest_id": "gst_scaffold", + "role": "primary_guest" + } + ] + } + ], + "itinerary": { + "start_date": "2026-01-01", + "end_date": "2026-01-05" + }, + "guests": [ + { + "id": "gst_scaffold", + "first_name": "Scaffold", + "last_name": "Guest" + } + ] +} diff --git a/scripts/scaffolds/lodging_booking_response.json b/scripts/scaffolds/lodging_booking_response.json new file mode 100644 index 000000000..a9c7df04d --- /dev/null +++ b/scripts/scaffolds/lodging_booking_response.json @@ -0,0 +1,79 @@ +{ + "ucp": { + "version": "2026-01-01", + "status": "success", + "capabilities": { + "dev.ucp.lodging.booking": [ + { "version": "2026-01-01" } + ] + }, + "payment_handlers": {} + }, + "id": "booking_scaffold", + "status": "incomplete", + "accommodation": { + "id": "acc_scaffold", + "name": "Scaffold Hotel", + "address": { + "street_address": "123 Scaffold Way", + "address_locality": "Scaffold City", + "address_region": "CA", + "address_country": "US", + "postal_code": "94043" + } + }, + "room_rates": [ + { + "id": "rr_scaffold", + "room_details": { + "id": "room_scaffold", + "title": "Scaffold Room", + "capacity": { + "adults": 2, + "total": 2 + } + }, + "rate_plan": { + "id": "rp_scaffold", + "title": "Scaffold Rate" + }, + "occupancy": { + "adults": 1, + "total": 1 + }, + "totals": [ + { + "type": "subtotal", + "amount": 1000 + }, + { + "type": "total", + "amount": 1000 + } + ] + } + ], + "itinerary": { + "start_date": "2026-01-01", + "end_date": "2026-01-05" + }, + "currency": "USD", + "totals": [ + { + "type": "subtotal", + "amount": 1000 + }, + { + "type": "total", + "amount": 1000 + } + ], + "links": [ + { + "type": "terms_of_service", + "url": "https://example.com/terms" + } + ], + "continue_url": "https://example.com/booking-sessions/scaffold", + "expires_at": "2026-01-01T12:00:00Z" +} diff --git a/source/schemas/common/payment_ap2_mandate.json b/source/schemas/common/payment_ap2_mandate.json index c9f5e4333..65a6f1d47 100644 --- a/source/schemas/common/payment_ap2_mandate.json +++ b/source/schemas/common/payment_ap2_mandate.json @@ -72,6 +72,35 @@ } ] }, + "dev.ucp.lodging.booking": { + "title": "Booking with AP2 Mandate", + "description": "Booking extended with AP2 mandate support.", + "allOf": [ + { + "$ref": "../lodging/booking.json" + }, + { + "type": "object", + "properties": { + "ap2": { + "allOf": [ + { + "$ref": "#/$defs/ap2_with_merchant_authorization" + }, + { + "$ref": "#/$defs/ap2_with_checkout_mandate" + } + ], + "ucp_request": { + "create": "omit", + "update": "omit", + "complete": "required" + } + } + } + } + ] + }, "error_code": { "title": "AP2 Error Code", "description": "Error codes specific to AP2 mandate verification.", diff --git a/source/schemas/common/payment_authentication.json b/source/schemas/common/payment_authentication.json index 34c77dc88..13f20ea7b 100644 --- a/source/schemas/common/payment_authentication.json +++ b/source/schemas/common/payment_authentication.json @@ -5,76 +5,69 @@ "title": "Payment Authentication Extension", "description": "Extends capabilities (e.g., checkout in retail shopping) with standard device data collection and 3DS challenge Action types used during payment authentication.", "$defs": { - "dev.ucp.shopping.checkout": { - "allOf": [ - { - "$ref": "../shopping/checkout.json" - }, - { + "payment_actions": { + "type": "object", + "properties": { + "actions": { "type": "object", + "ucp_request": "omit", "properties": { - "actions": { - "type": "object", - "ucp_request": "omit", - "properties": { - "dev.ucp.common.payment.device_data_collection": { - "type": "array", - "description": "A 3DS device data collection Action.", - "items": { + "dev.ucp.common.payment.device_data_collection": { + "type": "array", + "description": "A 3DS device data collection Action.", + "items": { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { "type": "object", "required": [ - "config" + "payment_instrument_id", + "url" ], "properties": { - "config": { - "type": "object", - "required": [ - "payment_instrument_id", - "url" - ], - "properties": { - "payment_instrument_id": { - "type": "string", - "minLength": 1, - "description": "ID of the payment instrument in the containing Checkout associated with this device data collection Action." - }, - "url": { - "type": "string", - "format": "uri", - "description": "URL for the invisible device data collection surface." - } - } + "payment_instrument_id": { + "type": "string", + "minLength": 1, + "description": "ID of the payment instrument in the containing Checkout associated with this device data collection Action." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL for the invisible device data collection surface." } } } - }, - "dev.ucp.common.payment.three_ds_challenge": { - "type": "array", - "description": "A 3DS challenge Action.", - "items": { + } + } + }, + "dev.ucp.common.payment.three_ds_challenge": { + "type": "array", + "description": "A 3DS challenge Action.", + "items": { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { "type": "object", "required": [ - "config" + "payment_instrument_id", + "url" ], "properties": { - "config": { - "type": "object", - "required": [ - "payment_instrument_id", - "url" - ], - "properties": { - "payment_instrument_id": { - "type": "string", - "minLength": 1, - "description": "ID of the payment instrument in the containing Checkout associated with this 3DS challenge Action." - }, - "url": { - "type": "string", - "format": "uri", - "description": "URL for the buyer-facing 3DS challenge surface." - } - } + "payment_instrument_id": { + "type": "string", + "minLength": 1, + "description": "ID of the payment instrument in the containing Checkout associated with this 3DS challenge Action." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL for the buyer-facing 3DS challenge surface." } } } @@ -83,6 +76,26 @@ } } } + } + }, + "dev.ucp.shopping.checkout": { + "allOf": [ + { + "$ref": "../shopping/checkout.json" + }, + { + "$ref": "#/$defs/payment_actions" + } + ] + }, + "dev.ucp.lodging.booking": { + "allOf": [ + { + "$ref": "../lodging/booking.json" + }, + { + "$ref": "#/$defs/payment_actions" + } ] } } diff --git a/source/schemas/common/payment_split_payments.json b/source/schemas/common/payment_split_payments.json index adcb77cef..bf1980ccf 100644 --- a/source/schemas/common/payment_split_payments.json +++ b/source/schemas/common/payment_split_payments.json @@ -53,6 +53,35 @@ } ] }, + "dev.ucp.lodging.booking": { + "title": "Booking with Split Payments", + "description": "Booking extended with split payment instrument amounts.", + "allOf": [ + { "$ref": "../lodging/booking.json" }, + { + "type": "object", + "properties": { + "payment": { + "type": "object", + "properties": { + "instruments": { + "type": "array", + "items": { + "$ref": "#/$defs/payment_instrument" + }, + "description": "Payment instruments in allocation priority order.", + "ucp_request": { + "create": "optional", + "update": "optional", + "complete": "required" + } + } + } + } + } + } + ] + }, "dev.ucp.common.payment.split_payments": { "business_schema": { "title": "Split Payments Capability (Business)", diff --git a/source/schemas/common/payment_terms.json b/source/schemas/common/payment_terms.json index f5e246d78..40c470957 100644 --- a/source/schemas/common/payment_terms.json +++ b/source/schemas/common/payment_terms.json @@ -80,6 +80,27 @@ } } ] + }, + "dev.ucp.lodging.booking": { + "title": "Booking with Payment Terms", + "description": "Booking extended with selectable payment terms.", + "allOf": [ + { "$ref": "../lodging/booking.json" }, + { + "type": "object", + "properties": { + "payment": { + "$ref": "#/$defs/payment", + "description": "Payment details with available and selected payment terms.", + "ucp_request": { + "create": "optional", + "update": "optional", + "complete": "required" + } + } + } + } + ] } } } diff --git a/source/schemas/common/types/date_interval.json b/source/schemas/common/types/date_interval.json new file mode 100644 index 000000000..2639f69d1 --- /dev/null +++ b/source/schemas/common/types/date_interval.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/common/types/date_interval.json", + "title": "Date Interval", + "type": "object", + "properties": { + "start_date": { + "type": "string", + "format": "date", + "description": "Start date of the interval in ISO 8601 format." + }, + "end_date": { + "type": "string", + "format": "date", + "description": "End date of the interval in ISO 8601 format." + } + } +} diff --git a/source/schemas/lodging/booking.json b/source/schemas/lodging/booking.json new file mode 100644 index 000000000..301a5a02d --- /dev/null +++ b/source/schemas/lodging/booking.json @@ -0,0 +1,179 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/lodging/booking.json", + "name": "dev.ucp.lodging.booking", + "title": "Booking", + "description": "Base booking schema. Extensions compose onto this using allOf.", + "type": "object", + "required": [ + "ucp", + "id", + "status", + "accommodation", + "room_rates", + "itinerary", + "currency", + "totals", + "links" + ], + "additionalProperties": true, + "properties": { + "ucp": { + "$ref": "../ucp.json#/$defs/response_booking_schema", + "ucp_request": "omit" + }, + "id": { + "type": "string", + "description": "Unique identifier of the booking.", + "ucp_request": "omit" + }, + "status": { + "type": "string", + "enum": [ + "incomplete", + "requires_escalation", + "ready_for_complete", + "complete_in_progress", + "completed", + "canceled" + ], + "description": "Booking state indicating the current phase and required action. See Booking Status lifecycle documentation for state transition details.", + "ucp_request": "omit" + }, + "accommodation": { + "$ref": "types/accommodation.json", + "description": "Accommodation location the booking is associated with.", + "ucp_request": { + "create": "required", + "update": "required", + "complete": "omit" + } + }, + "room_rates": { + "type": "array", + "items": { + "$ref": "types/room_rate.json" + }, + "description": "List of room and rate plan bindings for the booking.", + "ucp_request": { + "create": "required", + "update": "required", + "complete": "omit" + } + }, + "itinerary": { + "$ref": "../common/types/date_interval.json", + "description": "Duration of the booking itinerary.", + "ucp_request": { + "create": "required", + "update": "required", + "complete": "omit" + } + }, + "booker": { + "$ref": "types/booker.json", + "description": "Representation of the legal contracting party making the reservation.", + "ucp_request": { + "create": "optional", + "update": "optional", + "complete": "omit" + } + }, + "guests": { + "type": "array", + "items": { + "$ref": "types/guest.json" + }, + "description": "List of guest profiles associated with the booking.", + "ucp_request": { + "create": "optional", + "update": "optional", + "complete": "omit" + } + }, + "travel_purpose": { + "type": "string", + "description": "Purpose of the trip or reservation. Well-known values: `business`, `leisure`. Businesses MAY implement and support additional values.", + "examples": [ + "business", + "leisure" + ], + "ucp_request": { + "create": "optional", + "update": "optional", + "complete": "omit" + } + }, + "context": { + "$ref": "../common/types/context.json", + "ucp_request": { + "create": "optional", + "update": "optional", + "complete": "omit" + } + }, + "signals": { + "$ref": "../common/types/signals.json", + "ucp_request": "optional" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code reflecting the business's market determination. Derived from address, context, and geo IP—buyers provide signals, businesses determine currency.", + "ucp_request": "omit" + }, + "totals": { + "$ref": "../common/types/totals.json", + "description": "Booking totals.", + "ucp_request": "omit" + }, + "messages": { + "type": "array", + "items": { + "$ref": "../common/types/message.json" + }, + "description": "List of messages with error and info about the booking session state.", + "ucp_request": "omit" + }, + "links": { + "type": "array", + "items": { + "$ref": "../common/types/link.json" + }, + "description": "Links to be displayed by the platform (Privacy Policy, TOS). Mandatory for legal compliance.", + "ucp_request": "omit" + }, + "policies": { + "type": "array", + "items": { + "$ref": "../common/types/policy.json" + }, + "description": "Policies (e.g., cancellation terms) that apply to the booking session. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`.", + "ucp_request": "omit" + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "RFC 3339 expiry timestamp. Default TTL is 6 hours from start if not sent.", + "ucp_request": "omit" + }, + "continue_url": { + "type": "string", + "format": "uri", + "description": "URL for booking handoff and session recovery. MUST be provided when status is requires_escalation. See specification for format and availability requirements.", + "ucp_request": "omit" + }, + "payment": { + "$ref": "../common/types/payment.json", + "ucp_request": { + "create": "optional", + "update": "optional", + "complete": "required" + } + }, + "confirmation": { + "$ref": "types/booking_confirmation.json", + "description": "Details about the booking created from this specific session.", + "ucp_request": "omit" + } + } +} diff --git a/source/schemas/lodging/policy_cancellation.json b/source/schemas/lodging/policy_cancellation.json new file mode 100644 index 000000000..7fded23b1 --- /dev/null +++ b/source/schemas/lodging/policy_cancellation.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/lodging/policy_cancellation.json", + "title": "Lodging Cancellation Policy Extension", + "description": "Extends Booking with structured cancellation policy for lodging reservations.", + "type": "object", + "$defs": { + "cancellation_item": { + "allOf": [ + { + "$ref": "../common/types/policy.json" + }, + { + "type": "object", + "required": [ + "type", + "refundability" + ], + "properties": { + "type": { + "type": "string", + "const": "dev.ucp.lodging.policy.cancellation", + "description": "Policy discriminator for lodging cancellation terms." + }, + "refundability": { + "type": "string", + "description": "High-level refundability classification. Well-known values: `refundable` (free cancellation currently available), `partially_refundable` (cancellable with penalty, or inside penalty window), `non_refundable` (no refund upon cancellation).", + "examples": [ + "refundable", + "partially_refundable", + "non_refundable" + ] + } + } + } + ] + }, + "dev.ucp.lodging.booking": { + "title": "Booking with Cancellation Policy", + "description": "Booking capability extended with return policy terms on `policies[]`.", + "allOf": [ + { + "$ref": "booking.json" + }, + { + "type": "object", + "properties": { + "policies": { + "type": "array", + "items": { + "$ref": "#/$defs/cancellation_item" + }, + "ucp_request": "omit" + } + } + } + ] + } + } +} diff --git a/source/schemas/lodging/types/accommodation.json b/source/schemas/lodging/types/accommodation.json new file mode 100644 index 000000000..c1a1ed678 --- /dev/null +++ b/source/schemas/lodging/types/accommodation.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/lodging/types/accommodation.json", + "title": "Accommodation", + "description": "An accommodation (e.g., hotel, rental property) location.", + "type": "object", + "allOf": [ + { + "$ref": "../../common/types/location_summary.json" + }, + { + "type": "object", + "properties": { + "image_urls": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "List of accommodation image URIs.", + "ucp_request": "omit" + } + } + } + ] +} diff --git a/source/schemas/lodging/types/booker.json b/source/schemas/lodging/types/booker.json new file mode 100644 index 000000000..b214af98f --- /dev/null +++ b/source/schemas/lodging/types/booker.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/lodging/types/booker.json", + "title": "Booker", + "description": "Representation of the legal contracting party making the reservation.", + "type": "object", + "additionalProperties": true, + "properties": { + "first_name": { + "type": "string", + "description": "First name of the booker." + }, + "last_name": { + "type": "string", + "description": "Last name of the booker." + }, + "email": { + "type": "string", + "format": "email", + "description": "Email address of the booker." + }, + "phone_number": { + "type": "string", + "description": "E.164 standard. Phone number of the booker." + }, + "birthdate": { + "type": "string", + "format": "date", + "description": "Date of birth of the booker in ISO 8601 format." + }, + "company": { + "type": "string", + "description": "The booker's company or organization name, used for corporate invoicing." + }, + "address": { + "$ref": "../../common/types/postal_address.json", + "description": "Physical or registered address of the booker or company for legal contracting, regulatory compliance, and invoicing." + } + } +} diff --git a/source/schemas/lodging/types/booking_confirmation.json b/source/schemas/lodging/types/booking_confirmation.json new file mode 100644 index 000000000..cc9618e8c --- /dev/null +++ b/source/schemas/lodging/types/booking_confirmation.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/lodging/types/booking_confirmation.json", + "title": "Booking Confirmation", + "description": "Booking details available at the time of session completion.", + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "Unique booking identifier." + }, + "label": { + "type": "string", + "description": "Human-readable label (e.g., confirmation number) for identifying the booking if it differs from the unique id. MUST only be provided by the business." + }, + "pincode": { + "type": "string", + "description": "Additional code provided for secure access of the booking. SHOULD be used alongside the unique identifier or confirmation number." + }, + "permalink_url": { + "type": "string", + "format": "uri", + "description": "Permalink to access the booking directly on business's website." + } + } +} diff --git a/source/schemas/lodging/types/capacity.json b/source/schemas/lodging/types/capacity.json new file mode 100644 index 000000000..f3a3e7b18 --- /dev/null +++ b/source/schemas/lodging/types/capacity.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/lodging/types/capacity.json", + "title": "Capacity", + "type": "object", + "additionalProperties": true, + "properties": { + "adults": { + "type": "integer", + "description": "Number of adults.", + "minimum": 0 + }, + "children": { + "type": "array", + "items": { + "type": "object", + "properties": { + "from_age": { + "type": "integer", + "minimum": 0, + "description": "Lower bound of the age bracket." + }, + "to_age": { + "type": "integer", + "minimum": 0, + "description": "Upper bound of the age bracket." + }, + "total": { + "type": "integer", + "minimum": 0, + "description": "Capacity limit." + } + } + }, + "description": "Child capacity limits broken down by age brackets." + }, + "total": { + "type": "integer", + "description": "Total number of occupants. MUST be equal to adults + sum(children.total).", + "minimum": 0 + } + } +} diff --git a/source/schemas/lodging/types/guest.json b/source/schemas/lodging/types/guest.json new file mode 100644 index 000000000..b8c03ea6e --- /dev/null +++ b/source/schemas/lodging/types/guest.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/lodging/types/guest.json", + "title": "Guest", + "type": "object", + "additionalProperties": true, + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "Stable, opaque identifier for a guest in the Platform's namespace." + }, + "first_name": { + "type": "string", + "description": "First name of the guest." + }, + "last_name": { + "type": "string", + "description": "Last name of the guest." + }, + "email": { + "type": "string", + "description": "Email of the guest." + }, + "phone_number": { + "type": "string", + "description": "E.164 standard. Phone number of the guest." + }, + "age": { + "type": "integer", + "description": "Age of the guest, used to enforce any age-restriction for check-in and room occupancy.", + "minimum": 0 + } + } +} diff --git a/source/schemas/lodging/types/guest_assignment.json b/source/schemas/lodging/types/guest_assignment.json new file mode 100644 index 000000000..681ae1423 --- /dev/null +++ b/source/schemas/lodging/types/guest_assignment.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/lodging/types/guest_assignment.json", + "title": "Guest Assignment", + "description": "Assignment linking a room unit to a guest profile in the root guests pool.", + "type": "object", + "required": [ + "guest_id" + ], + "additionalProperties": true, + "properties": { + "guest_id": { + "type": "string", + "description": "Stable, opaque identifier of the assigned guest. MUST be consistent with platform supplied identifiers in the root `guests` pool." + }, + "role": { + "type": "string", + "description": "Role of the guest in this room. Well-known values: `primary_guest`, `adult`, and `child`." + } + } +} diff --git a/source/schemas/lodging/types/occupancy.json b/source/schemas/lodging/types/occupancy.json new file mode 100644 index 000000000..3db53c3b1 --- /dev/null +++ b/source/schemas/lodging/types/occupancy.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/lodging/types/occupancy.json", + "title": "Occupancy", + "description": "Occupancy breakdown and guest count for a booked room.", + "type": "object", + "required": [ + "total" + ], + "additionalProperties": true, + "properties": { + "adults": { + "type": "integer", + "minimum": 0, + "description": "Number of adults occupying the room." + }, + "children": { + "type": "integer", + "minimum": 0, + "description": "Number of children occupying the room." + }, + "child_ages": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0, + "maximum": 17 + }, + "description": "Ages of children occupying the room." + }, + "total": { + "type": "integer", + "minimum": 1, + "description": "Total number of occupants in the room." + } + } +} diff --git a/source/schemas/lodging/types/rate_plan.json b/source/schemas/lodging/types/rate_plan.json new file mode 100644 index 000000000..9130cc720 --- /dev/null +++ b/source/schemas/lodging/types/rate_plan.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/lodging/types/rate_plan.json", + "title": "Rate Plan", + "description": "Commercial rate contract.", + "type": "object", + "required": [ + "id", + "title" + ], + "additionalProperties": true, + "properties": { + "id": { + "type": "string", + "description": "Unique identifier of the rate plan.", + "ucp_request": { + "create": "optional", + "update": "optional", + "complete": "omit" + } + }, + "title": { + "type": "string", + "description": "Human-readable name of the rate plan.", + "ucp_request": "omit" + }, + "description": { + "type": "string", + "description": "Detailed description of the rate plan.", + "ucp_request": "omit" + } + } +} diff --git a/source/schemas/lodging/types/room_rate.json b/source/schemas/lodging/types/room_rate.json new file mode 100644 index 000000000..adeeb9867 --- /dev/null +++ b/source/schemas/lodging/types/room_rate.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/lodging/types/room_rate.json", + "title": "Room Rate", + "description": "Compound binding schema linking a physical room type, commercial rate plan, occupancy, guest assignments, optional add-ons, and pricing totals.", + "type": "object", + "required": [ + "id" + ], + "anyOf": [ + { + "properties": { + "id": { + "ucp_request": { + "create": "required", + "update": "required", + "complete": "omit" + } + } + } + }, + { + "required": [ + "room_type", + "rate_plan" + ], + "properties": { + "room_type": { + "properties": { + "id": { + "ucp_request": { + "create": "required", + "update": "required", + "complete": "omit" + } + } + } + }, + "rate_plan": { + "properties": { + "id": { + "ucp_request": { + "create": "required", + "update": "required", + "complete": "omit" + } + } + } + } + } + } + ], + "additionalProperties": true, + "properties": { + "id": { + "type": "string", + "description": "Stable, opaque unique identifier of the room rate binding.", + "ucp_request": { + "create": "optional", + "update": "optional", + "complete": "omit" + } + }, + "room_type": { + "$ref": "room_type.json", + "description": "Physical room real estate attributes." + }, + "rate_plan": { + "$ref": "rate_plan.json", + "description": "Commercial rate policy contract." + }, + "occupancy": { + "$ref": "occupancy.json", + "description": "Occupancy breakdown and guest count for this room." + }, + "guest_assignments": { + "type": "array", + "items": { + "$ref": "guest_assignment.json" + }, + "description": "List of guest assignments for this room." + }, + "totals": { + "$ref": "../../common/types/totals.json", + "description": "Totals breakdown for this room rate.", + "ucp_request": "omit" + } + } +} diff --git a/source/schemas/lodging/types/room_type.json b/source/schemas/lodging/types/room_type.json new file mode 100644 index 000000000..96dc238f8 --- /dev/null +++ b/source/schemas/lodging/types/room_type.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/lodging/types/room_type.json", + "title": "Room Type", + "description": "Physical room real estate attributes.", + "type": "object", + "required": [ + "id", + "title" + ], + "additionalProperties": true, + "properties": { + "id": { + "type": "string", + "description": "Unique identifier of the physical room type.", + "ucp_request": { + "create": "optional", + "update": "optional", + "complete": "omit" + } + }, + "title": { + "type": "string", + "description": "Human-readable title of the room type.", + "ucp_request": "omit" + }, + "description": { + "type": "string", + "description": "Detailed description of the room type.", + "ucp_request": "omit" + }, + "capacity": { + "$ref": "capacity.json", + "description": "Capacity details of the room type.", + "ucp_request": "omit" + }, + "image_urls": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "List of image URIs for the room type.", + "ucp_request": "omit" + } + } +} diff --git a/source/schemas/ucp.json b/source/schemas/ucp.json index b90e733c1..455aeb73e 100644 --- a/source/schemas/ucp.json +++ b/source/schemas/ucp.json @@ -270,6 +270,36 @@ ] }, + "response_booking_schema": { + "title": "UCP Booking Response Schema", + "description": "UCP metadata for booking responses.", + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "required": ["payment_handlers"], + "properties": { + "services": { + "additionalProperties": { + "items": { "$ref": "service.json#/$defs/response_schema" } + } + }, + "capabilities": { + "additionalProperties": { + "items": { "$ref": "capability.json#/$defs/response_schema" } + } + }, + "payment_handlers": { + "additionalProperties": { + "items": { + "$ref": "payment_handler.json#/$defs/response_schema" + } + } + } + } + } + ] + }, + "response_order_schema": { "title": "UCP Order Response Schema", "description": "UCP metadata for order responses. No payment handlers needed post-purchase.", diff --git a/source/services/lodging/mcp.openrpc.json b/source/services/lodging/mcp.openrpc.json new file mode 100644 index 000000000..c4e1634d7 --- /dev/null +++ b/source/services/lodging/mcp.openrpc.json @@ -0,0 +1,211 @@ +{ + "openrpc": "1.3.2", + "info": { + "title": "UCP Lodging Service", + "description": "Canonical MCP/JSON-RPC interface for UCP Lodging service. Schema references are logical pointers - actual payload shape is determined by negotiated capabilities.\n\n**Endpoint Resolution:** This spec defines methods only. The endpoint URL MUST be obtained from the business's discovery profile at `/.well-known/ucp` under `services[\"dev.ucp.lodging\"][transport=mcp].endpoint`. The server entry below is a placeholder for tooling compatibility." + }, + "servers": [ + { + "name": "business", + "url": "{endpoint}", + "description": "Business-provided endpoint from UCP discovery profile", + "variables": { + "endpoint": { + "default": "https://hotel.example.com/ucp/mcp", + "description": "Obtain from /.well-known/ucp → services[\"dev.ucp.lodging\"][transport=mcp].endpoint" + } + } + } + ], + "components": { + "schemas": { + "booking_session_result": { + "oneOf": [ + { + "$ref": "../../schemas/lodging/booking.json" + }, + { + "$ref": "../../schemas/common/types/error_response.json" + } + ] + }, + "meta": { + "type": "object", + "description": "Request metadata.", + "required": [ + "ucp_agent", + "idempotency_key" + ], + "additionalProperties": true, + "properties": { + "ucp_agent": { + "type": "object", + "description": "Platform agent identification. Maps to HTTP UCP-Agent header.", + "required": [ + "profile" + ], + "properties": { + "profile": { + "type": "string", + "format": "uri", + "description": "URL to the platform's UCP profile document." + } + } + }, + "idempotency_key": { + "type": "string", + "format": "uuid", + "description": "Unique key for retry safety. Maps to HTTP Idempotency-Key header." + } + } + } + } + }, + "methods": [ + { + "name": "create_booking_session", + "summary": "Create a booking session", + "description": "Create a new booking session.", + "params": [ + { + "name": "meta", + "required": true, + "schema": { + "$ref": "#/components/schemas/meta" + } + }, + { + "name": "booking", + "required": true, + "schema": { + "$ref": "../../schemas/lodging/booking.json" + } + } + ], + "result": { + "name": "booking", + "schema": { + "$ref": "#/components/schemas/booking_session_result" + } + } + }, + { + "name": "get_booking_session", + "summary": "Get booking session", + "params": [ + { + "name": "meta", + "required": true, + "schema": { + "$ref": "#/components/schemas/meta" + } + }, + { + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "booking", + "schema": { + "$ref": "#/components/schemas/booking_session_result" + } + } + }, + { + "name": "update_booking_session", + "summary": "Update booking session", + "params": [ + { + "name": "meta", + "required": true, + "schema": { + "$ref": "#/components/schemas/meta" + } + }, + { + "name": "id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "booking", + "required": true, + "schema": { + "$ref": "../../schemas/lodging/booking.json" + } + } + ], + "result": { + "name": "booking", + "schema": { + "$ref": "#/components/schemas/booking_session_result" + } + } + }, + { + "name": "complete_booking_session", + "summary": "Complete booking session and confirm room reservation", + "params": [ + { + "name": "meta", + "required": true, + "schema": { + "$ref": "#/components/schemas/meta" + } + }, + { + "name": "id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "booking", + "required": true, + "schema": { + "$ref": "../../schemas/lodging/booking.json" + } + } + ], + "result": { + "name": "booking", + "schema": { + "$ref": "#/components/schemas/booking_session_result" + } + } + }, + { + "name": "cancel_booking_session", + "summary": "Cancel booking session", + "params": [ + { + "name": "meta", + "required": true, + "schema": { + "$ref": "#/components/schemas/meta" + } + }, + { + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "booking", + "schema": { + "$ref": "#/components/schemas/booking_session_result" + } + } + } + ] +} diff --git a/source/services/lodging/rest.openapi.json b/source/services/lodging/rest.openapi.json new file mode 100644 index 000000000..257e22bdc --- /dev/null +++ b/source/services/lodging/rest.openapi.json @@ -0,0 +1,586 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "UCP Lodging Service", + "description": "Canonical REST interface for UCP Lodging service. Schema references are logical pointers - actual payload shape is determined by negotiated capabilities.\n\n**Endpoint Resolution:** This spec defines operations only. The base URL MUST be obtained from the business's discovery profile at `/.well-known/ucp` under `services[\"dev.ucp.lodging\"][transport=rest].endpoint`. The `{endpoint}` server variable below is a placeholder for tooling compatibility." + }, + "servers": [ + { + "url": "{endpoint}", + "description": "Business-provided endpoint from UCP discovery profile", + "variables": { + "endpoint": { + "default": "https://hotel.example.com/ucp", + "description": "Obtain from /.well-known/ucp → services[\"dev.ucp.lodging\"][transport=rest].endpoint" + } + } + } + ], + "paths": { + "/booking-sessions": { + "post": { + "operationId": "create_booking_session", + "summary": "Create Booking Session", + "description": "Create a new booking session.", + "parameters": [ + { + "$ref": "#/components/parameters/authorization" + }, + { + "$ref": "#/components/parameters/x_api_key" + }, + { + "$ref": "#/components/parameters/signature" + }, + { + "$ref": "#/components/parameters/signature_input" + }, + { + "$ref": "#/components/parameters/content_digest" + }, + { + "$ref": "#/components/parameters/idempotency_key" + }, + { + "$ref": "#/components/parameters/request_id" + }, + { + "$ref": "#/components/parameters/user_agent" + }, + { + "$ref": "#/components/parameters/ucp_agent" + }, + { + "$ref": "#/components/parameters/content_type" + }, + { + "$ref": "#/components/parameters/accept" + }, + { + "$ref": "#/components/parameters/accept_language" + }, + { + "$ref": "#/components/parameters/accept_encoding" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/booking" + } + } + } + }, + "responses": { + "201": { + "description": "Booking session created", + "headers": { + "Signature": { + "$ref": "#/components/headers/signature" + }, + "Signature-Input": { + "$ref": "#/components/headers/signature_input" + }, + "Content-Digest": { + "$ref": "#/components/headers/content_digest" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/booking_session_response" + } + } + } + } + } + } + }, + "/booking-sessions/{id}": { + "parameters": [ + { + "$ref": "#/components/parameters/booking_id_path" + } + ], + "get": { + "operationId": "get_booking_session", + "summary": "Get Booking Session", + "description": "Get the latest state of a booking session.", + "parameters": [ + { + "$ref": "#/components/parameters/authorization" + }, + { + "$ref": "#/components/parameters/x_api_key" + }, + { + "$ref": "#/components/parameters/signature" + }, + { + "$ref": "#/components/parameters/signature_input" + }, + { + "$ref": "#/components/parameters/request_id" + }, + { + "$ref": "#/components/parameters/user_agent" + }, + { + "$ref": "#/components/parameters/ucp_agent" + }, + { + "$ref": "#/components/parameters/content_type" + }, + { + "$ref": "#/components/parameters/accept" + }, + { + "$ref": "#/components/parameters/accept_language" + }, + { + "$ref": "#/components/parameters/accept_encoding" + } + ], + "responses": { + "200": { + "description": "Booking session retrieved", + "headers": { + "Signature": { + "$ref": "#/components/headers/signature" + }, + "Signature-Input": { + "$ref": "#/components/headers/signature_input" + }, + "Content-Digest": { + "$ref": "#/components/headers/content_digest" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/booking_session_response" + } + } + } + } + } + }, + "put": { + "operationId": "update_booking_session", + "summary": "Update Booking Session", + "description": "If an optional field is provided in the request body, its value is treated as a complete replacement for the corresponding data. If optional field is omitted, then current booking session is unchanged.", + "parameters": [ + { + "$ref": "#/components/parameters/authorization" + }, + { + "$ref": "#/components/parameters/x_api_key" + }, + { + "$ref": "#/components/parameters/signature" + }, + { + "$ref": "#/components/parameters/signature_input" + }, + { + "$ref": "#/components/parameters/content_digest" + }, + { + "$ref": "#/components/parameters/idempotency_key" + }, + { + "$ref": "#/components/parameters/request_id" + }, + { + "$ref": "#/components/parameters/user_agent" + }, + { + "$ref": "#/components/parameters/ucp_agent" + }, + { + "$ref": "#/components/parameters/content_type" + }, + { + "$ref": "#/components/parameters/accept" + }, + { + "$ref": "#/components/parameters/accept_language" + }, + { + "$ref": "#/components/parameters/accept_encoding" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/booking" + } + } + } + }, + "responses": { + "200": { + "description": "Booking session updated", + "headers": { + "Signature": { + "$ref": "#/components/headers/signature" + }, + "Signature-Input": { + "$ref": "#/components/headers/signature_input" + }, + "Content-Digest": { + "$ref": "#/components/headers/content_digest" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/booking_session_response" + } + } + } + } + } + } + }, + "/booking-sessions/{id}/complete": { + "parameters": [ + { + "$ref": "#/components/parameters/booking_id_path" + } + ], + "post": { + "operationId": "complete_booking_session", + "summary": "Complete Booking Session", + "description": "Complete booking session and confirm room reservation.", + "parameters": [ + { + "$ref": "#/components/parameters/authorization" + }, + { + "$ref": "#/components/parameters/x_api_key" + }, + { + "$ref": "#/components/parameters/signature" + }, + { + "$ref": "#/components/parameters/signature_input" + }, + { + "$ref": "#/components/parameters/content_digest" + }, + { + "$ref": "#/components/parameters/idempotency_key" + }, + { + "$ref": "#/components/parameters/request_id" + }, + { + "$ref": "#/components/parameters/user_agent" + }, + { + "$ref": "#/components/parameters/ucp_agent" + }, + { + "$ref": "#/components/parameters/content_type" + }, + { + "$ref": "#/components/parameters/accept" + }, + { + "$ref": "#/components/parameters/accept_language" + }, + { + "$ref": "#/components/parameters/accept_encoding" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/booking" + } + } + } + }, + "responses": { + "200": { + "description": "Booking session completed", + "headers": { + "Signature": { + "$ref": "#/components/headers/signature" + }, + "Signature-Input": { + "$ref": "#/components/headers/signature_input" + }, + "Content-Digest": { + "$ref": "#/components/headers/content_digest" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/booking_session_response" + } + } + } + } + } + } + }, + "/booking-sessions/{id}/cancel": { + "parameters": [ + { + "$ref": "#/components/parameters/booking_id_path" + } + ], + "post": { + "operationId": "cancel_booking_session", + "summary": "Cancel Booking Session", + "description": "Cancel an in-flight booking session", + "parameters": [ + { + "$ref": "#/components/parameters/authorization" + }, + { + "$ref": "#/components/parameters/x_api_key" + }, + { + "$ref": "#/components/parameters/signature" + }, + { + "$ref": "#/components/parameters/signature_input" + }, + { + "$ref": "#/components/parameters/content_digest" + }, + { + "$ref": "#/components/parameters/idempotency_key" + }, + { + "$ref": "#/components/parameters/request_id" + }, + { + "$ref": "#/components/parameters/user_agent" + }, + { + "$ref": "#/components/parameters/ucp_agent" + }, + { + "$ref": "#/components/parameters/content_type" + }, + { + "$ref": "#/components/parameters/accept" + }, + { + "$ref": "#/components/parameters/accept_language" + }, + { + "$ref": "#/components/parameters/accept_encoding" + } + ], + "responses": { + "200": { + "description": "Booking session canceled", + "headers": { + "Signature": { + "$ref": "#/components/headers/signature" + }, + "Signature-Input": { + "$ref": "#/components/headers/signature_input" + }, + "Content-Digest": { + "$ref": "#/components/headers/content_digest" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/booking_session_response" + } + } + } + } + } + } + } + }, + "components": { + "parameters": { + "booking_id_path": { + "name": "id", + "in": "path", + "required": true, + "description": "The unique identifier of the booking.", + "schema": { + "type": "string" + } + }, + "authorization": { + "name": "Authorization", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Should contain oauth token representing the following 2 schemes: 1. Platform self authenticating (client_credentials). 2. Platform authenticating on behalf of end user (authorization_code)." + }, + "x_api_key": { + "name": "X-API-Key", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Authenticates the platform with a reusable api key allocated to the platform by the business." + }, + "signature": { + "name": "Signature", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "RFC 9421 HTTP Message Signature. Required when using HTTP Message Signatures for authentication. Format: `sig1=::`." + }, + "signature_input": { + "name": "Signature-Input", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "RFC 9421 Signature-Input header. Required when using HTTP Message Signatures for authentication. Format: `sig1=(\"@method\" \"@path\" ...);created=;keyid=\"\"`." + }, + "content_digest": { + "name": "Content-Digest", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Body digest per RFC 9530. Format: `sha-256=::`." + }, + "idempotency_key": { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Ensures duplicate operations don't happen during retries." + }, + "request_id": { + "name": "Request-Id", + "in": "header", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "For tracing the requests across network layers and components." + }, + "user_agent": { + "name": "User-Agent", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Identifies the user agent string making the call." + }, + "ucp_agent": { + "name": "UCP-Agent", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "description": "Identifies the UCP agent making the call. All requests MUST include the UCP-Agent header containing the signer's profile URI using RFC 8941 Dictionary syntax. The URL MUST point to /.well-known/ucp. Format: profile=\"https://example.com/.well-known/ucp\"." + }, + "content_type": { + "name": "Content-Type", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Representation Metadata. Tells the receiver what the data in the message body actually is." + }, + "accept": { + "name": "Accept", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Content Negotiation. The client tells the server what data formats it is capable of understanding." + }, + "accept_language": { + "name": "Accept-Language", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Localization. Tells the receiver the user's preferred natural languages, often with \"weights\" or priorities." + }, + "accept_encoding": { + "name": "Accept-Encoding", + "in": "header", + "required": false, + "schema": { + "type": "string" + }, + "description": "Compression. The client tells the server which content-codings it supports, usually for compression." + } + }, + "headers": { + "signature": { + "required": false, + "schema": { + "type": "string" + }, + "description": "RFC 9421 HTTP Message Signature for response. Contains the signature value in the format `sig1=::`." + }, + "signature_input": { + "required": false, + "schema": { + "type": "string" + }, + "description": "RFC 9421 Signature-Input header for response. Describes signed components, timestamp, and key ID." + }, + "content_digest": { + "required": false, + "schema": { + "type": "string" + }, + "description": "Body digest per RFC 9530 for response." + } + }, + "schemas": { + "booking": { + "$ref": "../../schemas/lodging/booking.json" + }, + "booking_session_response": { + "oneOf": [ + { + "$ref": "../../schemas/lodging/booking.json" + }, + { + "$ref": "../../schemas/common/types/error_response.json" + } + ] + }, + "ucp": { + "$ref": "../../schemas/ucp.json" + } + } + } +} From 217907dc1686fc356b48272b370c8203b01df44e Mon Sep 17 00:00:00 2001 From: Jing Li Date: Tue, 1 Sep 2026 13:14:02 +0000 Subject: [PATCH 2/8] Minor fixes on typos or documentation consistency that were introduced due to design iterations. --- docs/specification/lodging/booking/index.md | 6 +++--- docs/specification/lodging/booking/mcp.md | 12 ++++++------ docs/specification/lodging/booking/rest.md | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/specification/lodging/booking/index.md b/docs/specification/lodging/booking/index.md index 2ae581448..57d3b99c9 100644 --- a/docs/specification/lodging/booking/index.md +++ b/docs/specification/lodging/booking/index.md @@ -220,7 +220,7 @@ Businesses **MUST** provide `continue_url` when returning `status` = ### Platform -* **MUST** supply valid `accommodation.id`, `room_rate.id`, `room_details.id`, +* **MUST** supply valid `accommodation.id`, `room_rate.id`, `room_type.id`, and `rate_plan.id` identifiers sourced from upper-funnel discovery mechanisms when creating a booking session. * **MUST** generate unique, stable string identifiers in the Platform namespace @@ -241,7 +241,7 @@ Businesses **MUST** provide `continue_url` when returning `status` = ### Business -* **MUST** evaluate requested `room_rate.id`, `room_details.id`, and `rate_plan.id` +* **MUST** evaluate requested `room_rate.id`, `room_type.id`, and `rate_plan.id` bindings against real-time availability and inventory constraints, echoing authoritative room metadata, pricing totals, and policy terms. * **MUST** preserve platform-supplied `guest.id` identifiers across session @@ -408,7 +408,7 @@ The `id` is discovered from upper-funnel search. ### Room Rate -Compound binding uniting physical room real estate (`room_details.id`), +Compound binding uniting physical room real estate (`room_type.id`), commercial rate terms (`rate_plan.id`), occupancy, and guest assignments. {{ schema_fields('types/room_rate', 'lodging/booking') }} diff --git a/docs/specification/lodging/booking/mcp.md b/docs/specification/lodging/booking/mcp.md index 76b7094bd..d015e6b89 100644 --- a/docs/specification/lodging/booking/mcp.md +++ b/docs/specification/lodging/booking/mcp.md @@ -81,7 +81,7 @@ protocol metadata: "id": 1, "method": "tools/call", "params": { - "name": "start_booking_session", + "name": "create_booking_session", "arguments": { "meta": { "ucp-agent": { @@ -364,7 +364,7 @@ Maps to the [Get Booking Session](index.md#get-booking-session) operation. {{ schema_fields('booking_resp', 'lodging/booking/mcp') }} -### `update_checkout` +### `update_booking_session` Maps to the [Update Booking](index.md#update-booking-session) operation. @@ -613,7 +613,7 @@ Maps to the [Complete Booking Session](index.md#complete-booking-session) operat #### Input Schema -* `id` (String): **Required**. The ID of the checkout session. +* `id` (String): **Required**. The ID of the booking session. {{ schema_fields('booking_complete_req', 'lodging/booking/mcp') }} @@ -895,7 +895,7 @@ as JSON-RPC `result` with `structuredContent` containing the UCP envelope and } }, "id": "booking_123", - "status": "ready_for_complete", + "status": "incomplete", "accommodation": { "id": "hotel_123", "name": "Beautiful Scenery Hotel" @@ -1007,7 +1007,7 @@ JSON-RPC `result` with `structuredContent` containing the UCP envelope and `mess ## Message Signing Platforms **SHOULD** authenticate agents when using MCP transport. When using -HTTP Message Signatures, all checkout operations follow the +HTTP Message Signatures, all booking operations follow the [Message Signatures](../../signatures.md) specification. ### Request Signing @@ -1074,7 +1074,7 @@ for the signing algorithm (identical for MCP over HTTP). A conforming MCP transport implementation **MUST**: 1. Implement JSON-RPC 2.0 protocol correctly. -2. Provide all core checkout tools defined in this specification. +2. Provide all core booking tools defined in this specification. 3. Return errors per the [Core Specification](../../overview/index.md#error-handling). 4. Return business outcomes as JSON-RPC `result` with UCP envelope and `messages` array. diff --git a/docs/specification/lodging/booking/rest.md b/docs/specification/lodging/booking/rest.md index 22fa1b8b3..1c3ce977f 100644 --- a/docs/specification/lodging/booking/rest.md +++ b/docs/specification/lodging/booking/rest.md @@ -1181,7 +1181,7 @@ with HTTP 200 and the UCP envelope containing `messages`: } }, "id": "booking_123", - "status": "ready_for_complete", + "status": "incomplete", "accommodation": { "id": "hotel_123", "name": "Beautiful Scenery Hotel" From 87976f7bbb394b4558911309fb840f9039e89814 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Sat, 5 Sep 2026 00:22:29 +0000 Subject: [PATCH 3/8] Address review feedback and augmenting existing specification with more examples. --- .cspell/custom-words.txt | 2 + docs/specification/lodging/booking/index.md | 745 +++++++++++++++++- docs/specification/lodging/booking/mcp.md | 37 +- docs/specification/lodging/booking/rest.md | 272 ++++++- docs/specification/overview/index.md | 2 + main.py | 18 +- mkdocs.yml | 5 +- .../schemas/common/types/date_interval.json | 11 +- source/schemas/common/types/policy.json | 2 +- source/schemas/lodging/booking.json | 13 +- .../schemas/lodging/policy_cancellation.json | 18 +- source/schemas/lodging/types/booker.json | 1 - source/schemas/lodging/types/capacity.json | 1 + source/schemas/lodging/types/guest.json | 1 + .../lodging/types/guest_assignment.json | 2 +- 15 files changed, 1036 insertions(+), 94 deletions(-) diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index b8a434b39..eb027ab3a 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -186,3 +186,5 @@ booker pincode refundability Refundability +ginza +Ginza diff --git a/docs/specification/lodging/booking/index.md b/docs/specification/lodging/booking/index.md index 57d3b99c9..09f6b772b 100644 --- a/docs/specification/lodging/booking/index.md +++ b/docs/specification/lodging/booking/index.md @@ -83,7 +83,11 @@ Booking follows a progressive session lifecycle: * **Platform-Generated Guest Identifiers (`guest.id`)**: Unlike business-scoped catalog and room identifiers, guest identifiers are generated, allocated, and managed by the Platform within the Platform's namespace. The Business treats - `guest.id` as a stable, opaque reference. + `guest.id` as a stable, opaque reference. Guest identifiers are opaque strings + scoped strictly to the individual booking session (e.g., `"gst_01"`, `"gst_02"`). + Platforms **MUST NOT** use persistent cross-merchant user tracking identifiers or + expose personally identifiable information (PII) within `guest.id`. Businesses + **MUST NOT** infer or link identity across distinct booking sessions based on `guest.id`. * **Guest Pool & Room Assignment Model**: Guest data is structured into a two-level relational model: * **Root Guest Pool (`guests[]`)**: A flat collection of all individual guest @@ -109,6 +113,179 @@ Booking follows a progressive session lifecycle: room capacity bounds, calculates totals (`totals[]`), and attaches authoritative cancellation terms (`policies[]`). +### Pricing Scope + +Lodging reservations follow strict all-in pricing rules to comply with consumer protection regulations (such as FTC and EU price transparency directives). In lodging, the full financial commitment for a stay is often divided between charges prepaid at the time of reservation confirmation and charges collected directly by the accommodation property upon check-in or check-out (e.g., resort fees, municipal occupancy taxes, or a remaining room balance). + +#### Pricing Architecture & Scope Guidelines + +* **Authoritative Root Total (`totals`)**: The top-level `totals` array represents the binding, authoritative pricing breakdown and aggregate financial commitment for the entire reservation stay across all requested room units. +* **Stay-Level Room Rate Total (`room_rates[].totals`)**: Each entry in `room_rates[].totals` reflects the total charges for that specific room rate unit across the entire itinerary stay duration (i.e., check-in to check-out), **NOT** a per-night figure. +* **Itemized Subtotals and Nightly Breakdown (`lines`)**: The `lines` array under a total item provides supplementary, itemized clarity: + * `subtotal` total items **MAY** carry `lines` representing the per-night room rate breakdown. + * `tax` total items **MAY** carry `lines` delineating separate tax authorities (e.g., state sales tax vs. local occupancy or tourism tax). + * `fee` total items **MAY** carry `lines` detailing mandatory charges (e.g., daily resort fees, cleaning fees). +* **Price Transparency and All-Inclusive Cost**: Platforms and Businesses **MUST** ensure that the guest is presented with the complete stay liability before booking confirmation. Hidden fees or undisclosed property charges violate price transparency standards. + +#### Immediate vs. Deferred Payment Breakdown + +* **Amount Due Now (`total`)**: The standard `type: "total"` entry strictly represents the immediate amount charged to the buyer's payment instrument upon booking confirmation. In full prepayment terms, this equals the entire cost of the stay. In deposit-based terms, this equals only the initial deposit and prepaid taxes/fees. The sum of all standard prepaid items (`subtotal` + prepaid `fee` + prepaid `tax` + `discount`) **MUST** equal `total`. +* **Deferred & Property-Collected Well-Known Types**: To model post-booking and property-collected charges consistently, the following well-known `type` values are introduced: + * `postpaid_subtotal`: The lodging room rate balance collected directly at the property (e.g., remaining room nights due at check-in after a partial deposit). + * `postpaid_fee`: Mandatory amenity, resort, cleaning, or facility fees collected directly by the property during the stay. + * `postpaid_tax`: Mandatory municipal, occupancy, or tourism taxes collected locally by the property (e.g., city accommodation tax). + * `due_at_property`: The aggregate sum of all property-collected charges (`postpaid_subtotal` + `postpaid_fee` + `postpaid_tax`). Present whenever any postpaid charges exist. + * `grand_total`: The all-inclusive stay cost (`total` + `due_at_property`), representing the Buyer's total financial commitment for the entire reservation. Present whenever deferred or property-collected amounts exist, ensuring full compliance with FTC and EU price display requirements. + * `grand_total`: The all-inclusive stay cost (`total` + `due_at_property`), representing the + +##### Well-Known Totals Types & Accounting Invariants + +| Type | Accounting Group | Description | Constraints & Invariants | +| :--- | :--- | :--- | :--- | +| `subtotal` | Immediate (Due Now) | Immediate room rate charges (or upfront deposit) | Exactly one entry in `totals[]` | +| `fee` | Immediate (Due Now) | Prepaid service, processing, or booking fees | Optional, repeatable | +| `tax` | Immediate (Due Now) | Prepaid state, value-added, or sales taxes | Optional, repeatable | +| `discount` | Immediate (Due Now) | Rate reductions, promotional discounts, or pay-now savings | Optional, negative amount | +| `total` | Immediate (Due Now) | Aggregate amount charged upon confirmation | Exactly one entry in `totals[]`; equals sum of immediate group | +| `postpaid_subtotal` | Deferred (Property) | Remaining room rate balance collected at hotel | Requires `display_text` | +| `postpaid_fee` | Deferred (Property) | Mandatory resort, facility, or cleaning fees paid at hotel | Requires `display_text` | +| `postpaid_tax` | Deferred (Property) | Municipal, city, or occupancy taxes paid at hotel | Requires `display_text` | +| `due_at_property` | Deferred (Property) | Aggregate total collected upon arrival/departure | Requires `display_text`; equals sum of deferred group | +| `grand_total` | Summary | Total stay liability across all payment timings | Requires `display_text`; equals `total` + `due_at_property` | + +#### Local Tax & Fee Disclosures + +When mandatory taxes or fees are collected locally by the lodging property and cannot be remitted at booking, the Business **SHOULD** provide a warning message in `messages[]` with `presentation: "disclosure"` and a `path` pointing directly to the relevant postpaid entry (e.g., `$.totals[4]`). This disclosure notice **MUST** state the applicable local rates, exemptions, and payment instructions, complemented by formal policy links in `links[]`. + +#### Payment Terms Integration (`dev.ucp.common.payment.terms`) + +When a Business supports flexible payment timing, it advertises the `dev.ucp.common.payment.terms` capability and populates `payment.terms[]` with the available payment terms alongside `payment.selected_term_id` indicating the active selection: + +* **Schedules and `totals` Alignment**: + * A payment term is composed of one or more `schedules[]`. + * Schedules with `type: "immediate"` represent payments due today upon booking completion and **MUST** sum to `totals[].type: "total"`. + * Schedules with `type: "deferred"` represent payments due at a specified future date or event (e.g., `due_at` timestamp or check-in) and **MUST** sum to `totals[].type: "due_at_property"`. +* **Selection Mutations**: + * When the Platform updates `payment.selected_term_id` via Update Booking Session (e.g., switching from "Pay now" to "First night now, balance at check-in"), the Business authoritatively recomputes `totals[]`. + * The recomputed `totals[]` reflects the newly selected term's immediate amount in `total`, partitions remaining nights into `postpaid_subtotal` and `postpaid_tax`, and updates `due_at_property` and `grand_total`. + +The following snippets illustrate how `totals[]` is structured across three canonical lodging pricing patterns: + +=== "Pattern 1: Property-Collected Charges" + + Prepaid room rate and service fee charged immediately (`total: 70400`), while resort fee and Tokyo Accommodation Tax are collected at check-in (`due_at_property: 6000`), totaling `grand_total: 76400`: + + + ```json + [ + { + "type": "subtotal", + "display_text": "Room Rate (3 nights for 1 room, includes 10% consumption tax)", + "amount": 64000 + }, + { + "type": "fee", + "display_text": "Service Fee (10% prepaid)", + "amount": 6400 + }, + { + "type": "total", + "display_text": "Total Due Now (Charged Today)", + "amount": 70400 + }, + { + "type": "postpaid_fee", + "display_text": "Resort & Facility Amenity Fee (Pay at hotel, ¥2,000/night)", + "amount": 6000 + }, + { + "type": "postpaid_tax", + "display_text": "Tokyo Accommodation Tax (Pay at hotel, ~¥200/guest/night)", + "amount": 0 + }, + { + "type": "due_at_property", + "display_text": "Total Due at Property (Pay upon Check-in)", + "amount": 6000 + }, + { + "type": "grand_total", + "display_text": "Grand Total (Total Stay Cost)", + "amount": 76400 + } + ] + ``` + +=== "Pattern 2: Upfront Payment with Savings" + + Upfront payment term selected (`pt_pay_now`), offering a $50 discount (`discount: -5000`) for paying in full today. No deferred balance remains (`due_at_property: 0`): + + + ```json + [ + { "type": "subtotal", "display_text": "Room Rate (3 nights)", "amount": 120000 }, + { "type": "fee", "display_text": "Service Fee", "amount": 3000 }, + { "type": "tax", "display_text": "State Lodging Tax (10%)", "amount": 12000 }, + { "type": "discount", "display_text": "Pay-now saving", "amount": -5000 }, + { "type": "total", "display_text": "Total Due Now (Charged Today)", "amount": 130000 }, + { "type": "due_at_property", "display_text": "Total Due at Property", "amount": 0 }, + { "type": "grand_total", "display_text": "Grand Total (Total Stay Cost)", "amount": 130000 } + ] + ``` + +=== "Pattern 3: Deposit & Check-in Balance" + + Deposit term selected (`pt_deposit_balance`), charging the 1st night deposit and initial taxes/fees today (`total: 47000`), with the remaining 2 nights rate and tax collected at check-in (`due_at_property: 88000`), yielding `grand_total: 135000`: + + + ```json + [ + { + "type": "subtotal", + "display_text": "Room Deposit (1st night, 1 room)", + "amount": 40000 + }, + { + "type": "fee", + "display_text": "Service Fee (Prepaid)", + "amount": 3000 + }, + { + "type": "tax", + "display_text": "State Lodging Tax on Deposit (10%)", + "amount": 4000 + }, + { + "type": "total", + "display_text": "Total Due Now (Deposit Charged Today)", + "amount": 47000 + }, + { + "type": "postpaid_subtotal", + "display_text": "Remaining Room Balance (2 nights @ $400, pay at hotel)", + "amount": 80000 + }, + { + "type": "postpaid_tax", + "display_text": "State Lodging Tax on Remaining Balance (10%, pay at hotel)", + "amount": 8000 + }, + { + "type": "due_at_property", + "display_text": "Total Due at Property (Pay upon Check-in)", + "amount": 88000 + }, + { + "type": "grand_total", + "display_text": "Grand Total (Total Stay Cost)", + "amount": 135000 + } + ] + ``` + +> [!TIP] +> For complete, end-to-end booking session payloads with room rate bindings, lead guest assignments, messages, and payment terms, see [Pricing & Payment Terms Examples](#pricing-examples). + ### Payments Payment handlers are discovered from the business's UCP profile at @@ -137,14 +314,37 @@ platform receives messages indicating what's needed to progress. inspect `messages` to understand what's needed. If any `recoverable` errors exist, resolve those first. Then hand off to user via `continue_url`. * **`ready_for_complete`**: Booking session has all necessary information - and platform can finalize programmatically. Platform can call - Complete Booking Session. + (confirmed pricing totals, valid itinerary dates, payment instrument + collected if required, lead guest identification via `booker` or + `primary_guest`, and all outstanding gating actions resolved) and platform + can finalize programmatically. Platform can call Complete Booking Session. * **`complete_in_progress`**: Business is processing the Complete Booking request. * **`completed`**: Booking confirmed successfully. * **`canceled`**: Booking session is invalid or expired. Platform should start a new booking session if needed. +### Actions + +When an active capability or extension has outstanding step-up work for the +booking session (such as PSD2 / 3D Secure strong customer authentication, +biometric step-up, or identity verification), the Business surfaces instances in +the response-only `actions` map. The common rules are defined in +[Overview — Actions](../../overview/index.md#actions); this section states how +the booking status lifecycle interprets them. + +Every Action gates the effect specified for its Action type. While `incomplete`, +an Action may identify work the Business needs completed before it can return +`ready_for_complete`. After processing the Action according to its Action type +contract (e.g., redirecting buyer via `url_redirect`), the Platform **SHOULD** use +Get Booking Session or a subsequent Update Booking Session to obtain the latest +booking state. + +If an Action prevents Complete Booking Session from being accepted, the Business +**MUST** return the current booking session with `status: incomplete` (or +`status: requires_escalation` if user handoff is required) and an error Message +with `severity: "recoverable"` whose `path` selects that exact Action occurrence. + ### Error Handling The `messages` array contains errors, warnings, and informational messages @@ -178,30 +378,20 @@ Both result in user handoff, but represent different booking session states: ### Warning Presentation -The `presentation` field on warning messages controls the rendering -contract the platform **MUST** follow. When omitted, it defaults to -`"notice"`. - -| | `notice` (default) | `disclosure` | -| :----------------------- | :----------------- | :-------------------------- | -| Display content | **MUST** | **MUST** | -| Proximity to `path` | **MAY** | **MUST** | -| Dismissible | **MAY** | **MUST NOT** | -| Render `image_url` | **MAY** | **MUST** | -| Render `url` | **MAY** | **SHOULD** | -| Escalate if cannot honor | — | **MUST** via `continue_url` | +The `presentation` field on warning messages controls the rendering contract the +platform **MUST** follow (e.g., `notice` vs. `disclosure`). For the authoritative +rendering rules, see [Checkout — Warning Presentation](../../shopping/checkout/index.md#warning-presentation). -#### `notice` (default) +#### Price Changes -The default rendering contract for warnings. Platforms **MUST** display -the warning content to the user. Platforms **MAY** render notices in a -banner, tray, or toast, and **MAY** allow the user to dismiss them. +When inventory rates or commercial pricing terms fluctuate during an active booking +session, the Business informs the Platform using a standard warning with +`code: "price_changed"` and `severity: "recoverable"`. -#### `disclosure` - -Warnings with `presentation: "disclosure"` carry notices — additional -terms & policies, compliance content, etc. — that **MUST** follow the -prescribed rendering contract. +The Business **MUST NOT** complete a booking session if unacknowledged price +changes occur. When a `price_changed` warning is received, the Platform **MUST** +present the updated totals to the buyer for explicit confirmation before submitting +a Complete Booking Session request. ## Continue URL @@ -220,9 +410,12 @@ Businesses **MUST** provide `continue_url` when returning `status` = ### Platform -* **MUST** supply valid `accommodation.id`, `room_rate.id`, `room_type.id`, - and `rate_plan.id` identifiers sourced from upper-funnel discovery mechanisms - when creating a booking session. +* **MUST** supply valid `accommodation.id`, and either a pre-composed `room_rate.id` + OR both `room_type.id` and `rate_plan.id` identifiers sourced from upper-funnel + discovery mechanisms when creating a booking session. +* **MUST** identify a lead guest by providing `booker` details or designating at + least one guest with `role: "primary_guest"` (including full legal name and contact + details) prior to invoking Complete Booking Session. * **MUST** generate unique, stable string identifiers in the Platform namespace for each entry in the root `guests[]` array (e.g., `"gst_01"`, `"gst_02"`). * **MUST** ensure every `guest_assignments[].guest_id` references a valid `id` @@ -241,16 +434,17 @@ Businesses **MUST** provide `continue_url` when returning `status` = ### Business -* **MUST** evaluate requested `room_rate.id`, `room_type.id`, and `rate_plan.id` - bindings against real-time availability and inventory constraints, echoing - authoritative room metadata, pricing totals, and policy terms. +* **MUST** evaluate requested `room_rate.id`, or the compound `room_type.id` and + `rate_plan.id` bindings against real-time availability and inventory constraints, + echoing authoritative room metadata, pricing totals, and policy terms. * **MUST** preserve platform-supplied `guest.id` identifiers across session updates and responses without remapping, renaming, or mutating them. * **MUST** validate that all `room_rates[].guest_assignments[].guest_id` references match an existing entry in the root `guests[]` array. * **MUST** enforce physical room `capacity` limits against the total assigned occupants and guest ages. -* **MUST** send a confirmation email after the booking has been completed. +* **MUST** send a confirmation email after the booking has been completed when a + valid email address is available in `booker` or primary guest details. * **SHOULD** provide accurate error and warning messages. * Logic handling the booking sessions **MUST** be deterministic. * **MUST** provide `continue_url` when returning `status` = `requires_escalation`. @@ -317,8 +511,497 @@ The abstract operations above are bound to specific transport protocols: * [REST Binding](rest.md): RESTful API mapping using standard HTTP verbs and JSON payloads. * [MCP Binding](mcp.md): Model Context Protocol mapping for agentic interaction. +## Examples {: #examples } + +### Pricing Examples + +The following examples provide complete, authoritative booking sessions demonstrating property-collected charges and flexible payment terms integration. + +=== "Property-Collected Taxes & Fees" + + A complete 3-night Tokyo hotel reservation where base room rates and service fees are prepaid immediately, while local Tokyo Accommodation Tax and resort fees are collected directly at check-in. Notice the tax disclosure warning in `messages[]` referencing the local tax line: + + + ```json + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.lodging.booking": [ + { + "version": "{{ ucp_version }}" + } + ] + }, + "payment_handlers": { + "com.example.card_handler": [ + { + "id": "card_handler", + "version": "{{ ucp_version }}", + "available_instruments": [ + { + "type": "card" + } + ] + } + ] + } + }, + "id": "bks_example_01", + "status": "incomplete", + "currency": "JPY", + "itinerary": { + "start_date": "2026-10-01", + "end_date": "2026-10-04" + }, + "accommodation": { + "id": "acc_ginza_hotel", + "name": "Ginza Grand Hotel" + }, + "room_rates": [ + { + "id": "rr_dlx_king_std", + "room_type": { + "id": "rt_dlx_king", + "title": "Deluxe King Room" + }, + "rate_plan": { + "id": "rp_standard", + "title": "Standard Flexible Rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + }, + "totals": [ + { + "type": "subtotal", + "amount": 64000 + }, + { + "type": "total", + "amount": 64000 + } + ] + } + ], + "totals": [ + { + "type": "subtotal", + "display_text": "Room Rate (3 nights for 1 room, also includes 10% consumption tax)", + "amount": 64000 + }, + { + "type": "fee", + "display_text": "Service Fee (10% prepaid)", + "amount": 6400 + }, + { + "type": "total", + "display_text": "Total Due Now (Charged Today)", + "amount": 70400 + }, + { + "type": "postpaid_fee", + "display_text": "Resort & Facility Amenity Fee (Pay at hotel, ¥2,000/night)", + "amount": 6000 + }, + { + "type": "postpaid_tax", + "display_text": "Tokyo Accommodation Tax (Pay at hotel, ~¥200/guest/night)", + "amount": 0 + }, + { + "type": "due_at_property", + "display_text": "Total Due at Property (Pay upon Check-in)", + "amount": 6000 + }, + { + "type": "grand_total", + "display_text": "Grand Total (Total Stay Cost)", + "amount": 76400 + } + ], + "messages": [ + { + "type": "warning", + "code": "local_tax", + "path": "$.totals[4]", + "presentation": "disclosure", + "content": "**Tokyo Accommodation Tax Notice**: In accordance with Tokyo Metropolitan Government regulations, a local accommodation tax of JPY 200 per guest per night applies to room rates of JPY 15,000 or higher. This tax is not included in the booking total and must be paid directly to the property upon check-in.", + "content_type": "markdown", + "url": "https://hotel.example.com/policies/tokyo-accommodation-tax" + } + ], + "links": [ + { + "type": "tax_policy", + "title": "Tokyo Local Accommodation Tax Schedule", + "url": "https://hotel.example.com/policies/tokyo-accommodation-tax" + }, + { + "type": "terms_of_service", + "title": "Hotel Booking Terms & Conditions", + "url": "https://hotel.example.com/terms" + }, + { + "type": "refund_policy", + "title": "Cancellation and Refund Policy", + "url": "https://hotel.example.com/cancellation-policy" + } + ] + } + ``` + +=== "Payment Terms — Pay Now" + + A complete booking session offering payment terms (`pt_pay_now` and `pt_deposit_balance`), where the buyer currently has `pt_pay_now` selected, paying $1,300.00 today and saving $50: + + + ```json + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.lodging.booking": [ + { + "version": "{{ ucp_version }}" + } + ], + "dev.ucp.common.payment.terms": [ + { + "version": "{{ ucp_version }}" + } + ] + }, + "payment_handlers": { + "com.example.card_handler": [ + { + "id": "card_handler", + "version": "{{ ucp_version }}", + "available_instruments": [ + { + "type": "card" + } + ] + } + ] + } + }, + "id": "bks_example_02", + "status": "incomplete", + "currency": "USD", + "itinerary": { + "start_date": "2026-09-01", + "end_date": "2026-09-04" + }, + "accommodation": { + "id": "acc_grand_hotel", + "name": "Grand Hotel" + }, + "room_rates": [ + { + "id": "rr_king_std", + "room_type": { + "id": "rt_king", + "title": "King Room" + }, + "rate_plan": { + "id": "rp_flex", + "title": "Flexible Rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + }, + "totals": [ + { + "type": "subtotal", + "amount": 120000 + }, + { + "type": "total", + "amount": 120000 + } + ] + } + ], + "totals": [ + { + "type": "subtotal", + "display_text": "Room Rate (3 nights)", + "amount": 120000 + }, + { + "type": "fee", + "display_text": "Service Fee", + "amount": 3000 + }, + { + "type": "tax", + "display_text": "State Lodging Tax (10%)", + "amount": 12000 + }, + { + "type": "discount", + "display_text": "Pay-now saving", + "amount": -5000 + }, + { + "type": "total", + "display_text": "Total Due Now (Charged Today)", + "amount": 130000 + }, + { + "type": "due_at_property", + "display_text": "Total Due at Property", + "amount": 0 + }, + { + "type": "grand_total", + "display_text": "Grand Total (Total Stay Cost)", + "amount": 130000 + } + ], + "links": [ + { + "type": "terms_of_service", + "title": "Terms of Service", + "url": "https://example.com/tos" + } + ], + "payment": { + "selected_term_id": "pt_pay_now", + "terms": [ + { + "id": "pt_pay_now", + "title": "Pay now", + "description": { + "plain": "Save $50 by paying for your stay today." + }, + "schedules": [ + { + "id": "sched_full", + "type": "immediate", + "description": { + "plain": "Due today when you book." + }, + "amount": 130000 + } + ] + }, + { + "id": "pt_deposit_balance", + "title": "First night now, balance at check-in", + "description": { + "plain": "Hold your room with one night's rate plus initial taxes & fees." + }, + "schedules": [ + { + "id": "sched_first_night", + "type": "immediate", + "description": { + "plain": "Due today when you book (Deposit: First night + service fee + tax)." + }, + "amount": 47000 + }, + { + "id": "sched_balance", + "type": "deferred", + "description": { + "plain": "Due at check-in on September 1, 2026 at 3:00 PM PDT (Remaining 2 nights + remaining tax)." + }, + "due_at": "2026-09-01T15:00:00-07:00", + "amount": 88000 + } + ] + } + ] + } + } + ``` + +=== "Payment Terms — Deposit & Balance" + + The recomputed booking session after selecting `pt_deposit_balance`, reflecting a $470.00 deposit charged today and an $880.00 balance due at check-in, for an all-in stay liability of $1,350.00: + + + ```json + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.lodging.booking": [ + { + "version": "{{ ucp_version }}" + } + ], + "dev.ucp.common.payment.terms": [ + { + "version": "{{ ucp_version }}" + } + ] + }, + "payment_handlers": { + "com.example.card_handler": [ + { + "id": "card_handler", + "version": "{{ ucp_version }}", + "available_instruments": [ + { + "type": "card" + } + ] + } + ] + } + }, + "id": "bks_example_03", + "status": "incomplete", + "currency": "USD", + "itinerary": { + "start_date": "2026-09-01", + "end_date": "2026-09-04" + }, + "accommodation": { + "id": "acc_grand_hotel", + "name": "Grand Hotel" + }, + "room_rates": [ + { + "id": "rr_king_std", + "room_type": { + "id": "rt_king", + "title": "King Room" + }, + "rate_plan": { + "id": "rp_flex", + "title": "Flexible Rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + }, + "totals": [ + { + "type": "subtotal", + "amount": 120000 + }, + { + "type": "total", + "amount": 120000 + } + ] + } + ], + "totals": [ + { + "type": "subtotal", + "display_text": "Room Deposit (1st night, 1 room)", + "amount": 40000 + }, + { + "type": "fee", + "display_text": "Service Fee (Prepaid)", + "amount": 3000 + }, + { + "type": "tax", + "display_text": "State Lodging Tax on Deposit (10%)", + "amount": 4000 + }, + { + "type": "total", + "display_text": "Total Due Now (Deposit Charged Today)", + "amount": 47000 + }, + { + "type": "postpaid_subtotal", + "display_text": "Remaining Room Balance (2 nights @ $400, pay at hotel)", + "amount": 80000 + }, + { + "type": "postpaid_tax", + "display_text": "State Lodging Tax on Remaining Balance (10%, pay at hotel)", + "amount": 8000 + }, + { + "type": "due_at_property", + "display_text": "Total Due at Property (Pay upon Check-in)", + "amount": 88000 + }, + { + "type": "grand_total", + "display_text": "Grand Total (Total Stay Cost)", + "amount": 135000 + } + ], + "links": [ + { + "type": "terms_of_service", + "title": "Terms of Service", + "url": "https://example.com/tos" + } + ], + "payment": { + "selected_term_id": "pt_deposit_balance", + "terms": [ + { + "id": "pt_pay_now", + "title": "Pay now", + "description": { + "plain": "Save $50 by paying for your stay today." + }, + "schedules": [ + { + "id": "sched_full", + "type": "immediate", + "description": { + "plain": "Due today when you book." + }, + "amount": 130000 + } + ] + }, + { + "id": "pt_deposit_balance", + "title": "First night now, balance at check-in", + "description": { + "plain": "Hold your room with one night's rate plus initial taxes & fees." + }, + "schedules": [ + { + "id": "sched_first_night", + "type": "immediate", + "description": { + "plain": "Due today when you book (Deposit: First night + service fee + tax)." + }, + "amount": 47000 + }, + { + "id": "sched_balance", + "type": "deferred", + "description": { + "plain": "Due at check-in on September 1, 2026 at 3:00 PM PDT (Remaining 2 nights + remaining tax)." + }, + "due_at": "2026-09-01T15:00:00-07:00", + "amount": 88000 + } + ] + } + ] + } + } + ``` + ## Entities +### Actions + +Step-up action directives required to progress the booking session (e.g., 3D Secure / PSD2 buyer redirection or identity verification). + +{{ schema_fields('types/actions', 'lodging/booking') }} + ### Accommodation Physical property details associated with the reservation. diff --git a/docs/specification/lodging/booking/mcp.md b/docs/specification/lodging/booking/mcp.md index d015e6b89..62dafa979 100644 --- a/docs/specification/lodging/booking/mcp.md +++ b/docs/specification/lodging/booking/mcp.md @@ -256,15 +256,15 @@ Maps to the [Create Booking Session](index.md#create-booking-session) operation. "totals": [ { "type": "subtotal", - "amount": 55000 + "amount": 330000 }, { "type": "tax", - "amount": 5500 + "amount": 33000 }, { "type": "total", - "amount": 60500 + "amount": 363000 } ] } @@ -277,11 +277,11 @@ Maps to the [Create Booking Session](index.md#create-booking-session) operation. "totals": [ { "type": "subtotal", - "amount": 385000 + "amount": 330000 }, { "type": "tax", - "amount": 38500 + "amount": 33000 }, { "type": "fee", @@ -290,7 +290,7 @@ Maps to the [Create Booking Session](index.md#create-booking-session) operation. }, { "type": "total", - "amount": 424500 + "amount": 364000 } ], "links": [ @@ -529,15 +529,15 @@ Maps to the [Update Booking](index.md#update-booking-session) operation. "totals": [ { "type": "subtotal", - "amount": 55000 + "amount": 330000 }, { "type": "tax", - "amount": 5500 + "amount": 33000 }, { "type": "total", - "amount": 60500 + "amount": 363000 } ] } @@ -564,11 +564,11 @@ Maps to the [Update Booking](index.md#update-booking-session) operation. "totals": [ { "type": "subtotal", - "amount": 385000 + "amount": 330000 }, { "type": "tax", - "amount": 38500 + "amount": 33000 }, { "type": "fee", @@ -577,7 +577,7 @@ Maps to the [Update Booking](index.md#update-booking-session) operation. }, { "type": "total", - "amount": 424500 + "amount": 364000 } ], "links": [ @@ -759,15 +759,15 @@ Maps to the [Complete Booking Session](index.md#complete-booking-session) operat "totals": [ { "type": "subtotal", - "amount": 55000 + "amount": 330000 }, { "type": "tax", - "amount": 5500 + "amount": 33000 }, { "type": "total", - "amount": 60500 + "amount": 363000 } ] } @@ -794,11 +794,11 @@ Maps to the [Complete Booking Session](index.md#complete-booking-session) operat "totals": [ { "type": "subtotal", - "amount": 385000 + "amount": 330000 }, { "type": "tax", - "amount": 38500 + "amount": 33000 }, { "type": "fee", @@ -807,7 +807,7 @@ Maps to the [Complete Booking Session](index.md#complete-booking-session) operat }, { "type": "total", - "amount": 424500 + "amount": 364000 } ], "links": [ @@ -949,6 +949,7 @@ as JSON-RPC `result` with `structuredContent` containing the UCP envelope and }, { "type": "fee", + "display_text": "Resort fee", "amount": 32500 }, { diff --git a/docs/specification/lodging/booking/rest.md b/docs/specification/lodging/booking/rest.md index 1c3ce977f..48ab71b58 100644 --- a/docs/specification/lodging/booking/rest.md +++ b/docs/specification/lodging/booking/rest.md @@ -103,6 +103,8 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version ### Create Booking Session +#### Single-room Booking + === "Request" @@ -160,7 +162,7 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version } }, "id": "booking_123", - "status": "ready_for_complete", + "status": "incomplete", "accommodation": { "id": "hotel_123", "name": "Beautiful Scenery Hotel", @@ -206,15 +208,15 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version "totals": [ { "type": "subtotal", - "amount": 55000 + "amount": 330000 }, { "type": "tax", - "amount": 5500 + "amount": 33000 }, { "type": "total", - "amount": 60500 + "amount": 363000 } ] } @@ -227,11 +229,11 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version "totals": [ { "type": "subtotal", - "amount": 385000 + "amount": 330000 }, { "type": "tax", - "amount": 38500 + "amount": 33000 }, { "type": "fee", @@ -240,7 +242,7 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version }, { "type": "total", - "amount": 424500 + "amount": 364000 } ], "links": [ @@ -288,6 +290,213 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version } ``` +#### Multi-room Booking + +=== "Request" + + + ```json + POST /booking-sessions HTTP/1.1 + UCP-Agent: profile="https://platform.example/profile" + Content-Type: application/json + ...other required headers... + + { + "accommodation": { + "id": "hotel_123" + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen" + }, + "rate_plan": { + "id": "rp_avg_base_rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + } + }, + { + "id": "rt_standard_king__rp_avg_base_rate", + "room_type": { + "id": "rt_standard_king" + }, + "rate_plan": { + "id": "rp_avg_base_rate" + }, + "occupancy": { + "adults": 1, + "total": 1 + } + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + } + } + ``` + +=== "Response" + + + ```json + HTTP/1.1 201 Created + Content-Type: application/json + + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.lodging.booking": [ + {"version": "{{ ucp_version }}"} + ] + }, + "payment_handlers": { + "com.example.vendor.delegate_payment": [ + {"id": "handler_1", "version": "{{ ucp_version }}", "available_instruments": [{"type": "card"}], "config": {}} + ] + } + }, + "id": "booking_124", + "status": "incomplete", + "accommodation": { + "id": "hotel_123", + "name": "Beautiful Scenery Hotel", + "address": { + "street_address": "123 Scene St", + "address_locality": "Phoenix", + "address_region": "AZ", + "address_country": "US", + "postal_code": "85004" + } + }, + "room_rates": [ + { + "id": "rt_luxury_queen__rp_avg_base_rate", + "room_type": { + "id": "rt_luxury_queen", + "title": "Luxury Queen Room with Two Queen Beds", + "capacity": { + "adults": 2, + "children": [ + { + "from_age": 0, + "to_age": 5, + "total": 1 + }, + { + "from_age": 6, + "to_age": 16, + "total": 1 + } + ], + "total": 4 + } + }, + "rate_plan": { + "id": "rp_avg_base_rate", + "title": "Best Available Rate" + }, + "occupancy": { + "adults": 2, + "total": 2 + }, + "totals": [ + { + "type": "subtotal", + "amount": 330000 + }, + { + "type": "tax", + "amount": 33000 + }, + { + "type": "total", + "amount": 363000 + } + ] + }, + { + "id": "rt_standard_king__rp_avg_base_rate", + "room_type": { + "id": "rt_standard_king", + "title": "Standard King Room", + "capacity": { + "adults": 2, + "total": 2 + } + }, + "rate_plan": { + "id": "rp_avg_base_rate", + "title": "Best Available Rate" + }, + "occupancy": { + "adults": 1, + "total": 1 + }, + "totals": [ + { + "type": "subtotal", + "amount": 270000 + }, + { + "type": "tax", + "amount": 27000 + }, + { + "type": "total", + "amount": 297000 + } + ] + } + ], + "itinerary": { + "start_date": "2026-07-15", + "end_date": "2026-07-21" + }, + "currency": "USD", + "totals": [ + { + "type": "subtotal", + "amount": 600000 + }, + { + "type": "tax", + "amount": 60000 + }, + { + "type": "fee", + "display_text": "Booking fee", + "amount": 1000 + }, + { + "type": "total", + "amount": 661000 + } + ], + "links": [ + { + "type": "privacy_policy", + "url": "https://business.example.com/privacy" + }, + { + "type": "terms_of_service", + "url": "https://business.example.com/terms" + }, + { + "type": "cancellation_policy", + "url": "https://business.example.com/cancellation" + } + ], + "continue_url": "https://business.example.com/booking-sessions/booking_124", + "expires_at": "2026-06-01T18:30:00Z" + } + ``` + ### Update Booking Session Update calls allow clients to progressively build optional fields @@ -452,15 +661,15 @@ place to set these expectations via `messages`. "totals": [ { "type": "subtotal", - "amount": 55000 + "amount": 330000 }, { "type": "tax", - "amount": 5500 + "amount": 33000 }, { "type": "total", - "amount": 60500 + "amount": 363000 } ] } @@ -502,11 +711,11 @@ place to set these expectations via `messages`. "totals": [ { "type": "subtotal", - "amount": 385000 + "amount": 330000 }, { "type": "tax", - "amount": 38500 + "amount": 33000 }, { "type": "fee", @@ -515,7 +724,7 @@ place to set these expectations via `messages`. }, { "type": "total", - "amount": 424500 + "amount": 364000 } ], "links": [ @@ -660,15 +869,15 @@ place to set these expectations via `messages`. "totals": [ { "type": "subtotal", - "amount": 55000 + "amount": 330000 }, { "type": "tax", - "amount": 5500 + "amount": 33000 }, { "type": "total", - "amount": 60500 + "amount": 363000 } ] } @@ -710,11 +919,11 @@ place to set these expectations via `messages`. "totals": [ { "type": "subtotal", - "amount": 385000 + "amount": 330000 }, { "type": "tax", - "amount": 38500 + "amount": 33000 }, { "type": "fee", @@ -723,7 +932,7 @@ place to set these expectations via `messages`. }, { "type": "total", - "amount": 424500 + "amount": 364000 } ], "links": [ @@ -843,15 +1052,15 @@ place to set these expectations via `messages`. "totals": [ { "type": "subtotal", - "amount": 55000 + "amount": 330000 }, { "type": "tax", - "amount": 5500 + "amount": 33000 }, { "type": "total", - "amount": 60500 + "amount": 363000 } ] } @@ -893,11 +1102,11 @@ place to set these expectations via `messages`. "totals": [ { "type": "subtotal", - "amount": 385000 + "amount": 330000 }, { "type": "tax", - "amount": 38500 + "amount": 33000 }, { "type": "fee", @@ -906,7 +1115,7 @@ place to set these expectations via `messages`. }, { "type": "total", - "amount": 424500 + "amount": 364000 } ], "links": [ @@ -1026,15 +1235,15 @@ place to set these expectations via `messages`. "totals": [ { "type": "subtotal", - "amount": 55000 + "amount": 330000 }, { "type": "tax", - "amount": 5500 + "amount": 33000 }, { "type": "total", - "amount": 60500 + "amount": 363000 } ] } @@ -1076,11 +1285,11 @@ place to set these expectations via `messages`. "totals": [ { "type": "subtotal", - "amount": 385000 + "amount": 330000 }, { "type": "tax", - "amount": 38500 + "amount": 33000 }, { "type": "fee", @@ -1089,7 +1298,7 @@ place to set these expectations via `messages`. }, { "type": "total", - "amount": 424500 + "amount": 364000 } ], "links": [ @@ -1235,6 +1444,7 @@ with HTTP 200 and the UCP envelope containing `messages`: }, { "type": "fee", + "display_text": "Resort fee", "amount": 32500 }, { diff --git a/docs/specification/overview/index.md b/docs/specification/overview/index.md index df8bac3a6..d594cc9f1 100644 --- a/docs/specification/overview/index.md +++ b/docs/specification/overview/index.md @@ -841,6 +841,7 @@ All capability and service names **MUST** use the format: | ----------------------------------- | ----------- | -------- | ---------------- | | `dev.ucp.shopping.checkout` | ucp.dev | shopping | checkout | | `dev.ucp.shopping.fulfillment` | ucp.dev | shopping | fulfillment | +| `dev.ucp.lodging.booking` | ucp.dev | lodging | booking | | `dev.ucp.common.identity_linking` | ucp.dev | common | identity_linking | | `com.example.payments.installments` | example.com | payments | installments | @@ -3084,6 +3085,7 @@ A Business publishes well-known and custom policies. Every policy carries a | :-- | :-- | | `dev.ucp.shopping.policy.return` | Return terms. | | `dev.ucp.shopping.policy.warranty` | Warranty terms. | +| `dev.ucp.lodging.policy.cancellation` | Cancellation terms for lodging reservations. | A Business **MAY** define custom types in its own domain (e.g., `com.example.policy.price_match`) and **MAY** add type-specific fields that a diff --git a/main.py b/main.py index 47068d512..5d74b8968 100644 --- a/main.py +++ b/main.py @@ -609,8 +609,9 @@ def _render_embedded_table( ) ) - # When allOf composition overrides a property from an earlier branch, - # prefer the outer (later) definition and render each field only once. + # When allOf composition overrides a property from an earlier branch, prefer + # the outer (later) definition per cell, falling back to the base branch for + # cells the specialization leaves empty. Render each field only once. deduped_rows = {} other_lines = [] for block in md: @@ -623,11 +624,20 @@ def _render_embedded_table( # Exclude header and separator rows if any were embedded if len(parts) >= 5 and parts[1] not in ("Name", ":---"): field_name = parts[1] - deduped_rows[field_name] = line + prior = deduped_rows.get(field_name) + if prior is None: + deduped_rows[field_name] = parts + else: + # "any" is the renderer's placeholder for a branch that adds no + # `type`, so it defers to the base branch just like an empty cell. + deduped_rows[field_name] = [ + new if new and new != "any" else (old or new) + for new, old in zip(parts, prior, strict=False) + ] continue other_lines.append(line) - result = list(deduped_rows.values()) + result = ["| " + " | ".join(p[1:-1]) + " |" for p in deduped_rows.values()] if other_lines: result.extend(other_lines) diff --git a/mkdocs.yml b/mkdocs.yml index 9585b6e7e..178b14a50 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -552,7 +552,10 @@ plugins: JSON-RPC tools for AI booking agents. Lodging Extensions: - specification/lodging/extensions/cancellation-policy.md: >- - TODO: add content + Lodging Cancellation Policy Extension, defining the + dev.ucp.lodging.policy.cancellation type on the core policies[] + primitive with a machine-readable refundability classification and + a human-readable penalty schedule for lodging reservations. Payment: - specification/payment/extensions/ap2-mandates.md: >- AP2 Mandates Extension, detailing secure, cryptographically bound diff --git a/source/schemas/common/types/date_interval.json b/source/schemas/common/types/date_interval.json index 2639f69d1..d9ed51f40 100644 --- a/source/schemas/common/types/date_interval.json +++ b/source/schemas/common/types/date_interval.json @@ -2,7 +2,16 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/common/types/date_interval.json", "title": "Date Interval", + "description": "Represents a closed date range defined by start and end dates.", "type": "object", + "dependentRequired": { + "start_date": [ + "end_date" + ], + "end_date": [ + "start_date" + ] + }, "properties": { "start_date": { "type": "string", @@ -12,7 +21,7 @@ "end_date": { "type": "string", "format": "date", - "description": "End date of the interval in ISO 8601 format." + "description": "End date of the interval in ISO 8601 format. MUST be on or after `start_date`." } } } diff --git a/source/schemas/common/types/policy.json b/source/schemas/common/types/policy.json index 40d28b1e7..7ab2841fe 100644 --- a/source/schemas/common/types/policy.json +++ b/source/schemas/common/types/policy.json @@ -12,7 +12,7 @@ "properties": { "type": { "$ref": "reverse_domain_name.json", - "description": "Policy type discriminator. Open reverse-DNS vocabulary. Well-known values: `dev.ucp.shopping.policy.return` (return terms), `dev.ucp.shopping.policy.warranty` (warranty terms). Businesses MAY define custom types in their own domain (e.g., `com.example.policy.price_match`). Platforms MUST tolerate unknown values." + "description": "Policy type discriminator. Open reverse-DNS vocabulary. See specification documentation for the registry of well-known policy types. Businesses MAY define custom types in their own domain (e.g., `com.example.policy.price_match`). Platforms MUST tolerate unknown values." }, "description": { "$ref": "description.json", diff --git a/source/schemas/lodging/booking.json b/source/schemas/lodging/booking.json index 301a5a02d..18f779243 100644 --- a/source/schemas/lodging/booking.json +++ b/source/schemas/lodging/booking.json @@ -64,6 +64,10 @@ "itinerary": { "$ref": "../common/types/date_interval.json", "description": "Duration of the booking itinerary.", + "required": [ + "start_date", + "end_date" + ], "ucp_request": { "create": "required", "update": "required", @@ -123,7 +127,12 @@ }, "totals": { "$ref": "../common/types/totals.json", - "description": "Booking totals.", + "description": "Authoritative pricing breakdown for the booking. The `total` entry represents the immediate amount payable upon confirmation (Due Now). When property-collected or deferred charges apply, businesses provide `postpaid_*` entries, `due_at_property`, and `grand_total` to convey the all-inclusive stay liability in compliance with price transparency directives.", + "ucp_request": "omit" + }, + "actions": { + "$ref": "../common/types/actions.json", + "description": "Outstanding extension-defined Actions for this booking.", "ucp_request": "omit" }, "messages": { @@ -153,7 +162,7 @@ "expires_at": { "type": "string", "format": "date-time", - "description": "RFC 3339 expiry timestamp. Default TTL is 6 hours from start if not sent.", + "description": "RFC 3339 timestamp after which the booking session is no longer valid. Quoted pricing and availability are held until this time; businesses MUST NOT complete a booking at a modified price without first returning updated totals to the platform.", "ucp_request": "omit" }, "continue_url": { diff --git a/source/schemas/lodging/policy_cancellation.json b/source/schemas/lodging/policy_cancellation.json index 7fded23b1..b0fd5579b 100644 --- a/source/schemas/lodging/policy_cancellation.json +++ b/source/schemas/lodging/policy_cancellation.json @@ -1,9 +1,9 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/lodging/policy_cancellation.json", + "name": "dev.ucp.lodging.policy.cancellation", "title": "Lodging Cancellation Policy Extension", "description": "Extends Booking with structured cancellation policy for lodging reservations.", - "type": "object", "$defs": { "cancellation_item": { "allOf": [ @@ -37,7 +37,7 @@ }, "dev.ucp.lodging.booking": { "title": "Booking with Cancellation Policy", - "description": "Booking capability extended with return policy terms on `policies[]`.", + "description": "Booking capability extended with cancellation policy terms on `policies[]`.", "allOf": [ { "$ref": "booking.json" @@ -48,7 +48,19 @@ "policies": { "type": "array", "items": { - "$ref": "#/$defs/cancellation_item" + "if": { + "required": [ + "type" + ], + "properties": { + "type": { + "const": "dev.ucp.lodging.policy.cancellation" + } + } + }, + "then": { + "$ref": "#/$defs/cancellation_item" + } }, "ucp_request": "omit" } diff --git a/source/schemas/lodging/types/booker.json b/source/schemas/lodging/types/booker.json index b214af98f..f8dda229c 100644 --- a/source/schemas/lodging/types/booker.json +++ b/source/schemas/lodging/types/booker.json @@ -16,7 +16,6 @@ }, "email": { "type": "string", - "format": "email", "description": "Email address of the booker." }, "phone_number": { diff --git a/source/schemas/lodging/types/capacity.json b/source/schemas/lodging/types/capacity.json index f3a3e7b18..692927a86 100644 --- a/source/schemas/lodging/types/capacity.json +++ b/source/schemas/lodging/types/capacity.json @@ -2,6 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/lodging/types/capacity.json", "title": "Capacity", + "description": "Physical room capacity and allowed guest age rules.", "type": "object", "additionalProperties": true, "properties": { diff --git a/source/schemas/lodging/types/guest.json b/source/schemas/lodging/types/guest.json index b8c03ea6e..070f75841 100644 --- a/source/schemas/lodging/types/guest.json +++ b/source/schemas/lodging/types/guest.json @@ -2,6 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/lodging/types/guest.json", "title": "Guest", + "description": "Profile of an individual guest staying at the accommodation.", "type": "object", "additionalProperties": true, "required": [ diff --git a/source/schemas/lodging/types/guest_assignment.json b/source/schemas/lodging/types/guest_assignment.json index 681ae1423..6823c9a30 100644 --- a/source/schemas/lodging/types/guest_assignment.json +++ b/source/schemas/lodging/types/guest_assignment.json @@ -15,7 +15,7 @@ }, "role": { "type": "string", - "description": "Role of the guest in this room. Well-known values: `primary_guest`, `adult`, and `child`." + "description": "Role of the guest in this room. Well-known values: `primary_guest` (the guest the reservation is attached to), `additional_guest`. Businesses MAY define additional values; platforms MUST tolerate unknown values." } } } From cc10ae804c4fd1ae995c9f0330b4764b9d4a1445 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Sat, 5 Sep 2026 00:56:15 +0000 Subject: [PATCH 4/8] Minor formatting cleanups. --- docs/specification/lodging/booking/index.md | 106 +++++++++++++++----- 1 file changed, 80 insertions(+), 26 deletions(-) diff --git a/docs/specification/lodging/booking/index.md b/docs/specification/lodging/booking/index.md index 09f6b772b..367d20e02 100644 --- a/docs/specification/lodging/booking/index.md +++ b/docs/specification/lodging/booking/index.md @@ -88,6 +88,9 @@ Booking follows a progressive session lifecycle: Platforms **MUST NOT** use persistent cross-merchant user tracking identifiers or expose personally identifiable information (PII) within `guest.id`. Businesses **MUST NOT** infer or link identity across distinct booking sessions based on `guest.id`. + * **Timing**: Platform **SHOULD NOT** send `guests[]` identity fields beyond `id` before the + booking reaches `ready_for_complete`, and **SHOULD** send only the fields the business + requests via `messages[]`. * **Guest Pool & Room Assignment Model**: Guest data is structured into a two-level relational model: * **Root Guest Pool (`guests[]`)**: A flat collection of all individual guest @@ -115,28 +118,52 @@ Booking follows a progressive session lifecycle: ### Pricing Scope -Lodging reservations follow strict all-in pricing rules to comply with consumer protection regulations (such as FTC and EU price transparency directives). In lodging, the full financial commitment for a stay is often divided between charges prepaid at the time of reservation confirmation and charges collected directly by the accommodation property upon check-in or check-out (e.g., resort fees, municipal occupancy taxes, or a remaining room balance). +Lodging reservations follow strict all-in pricing rules to comply with consumer protection regulations +(such as FTC and EU price transparency directives). In lodging, the full financial commitment for a stay +is often divided between charges prepaid at the time of reservation confirmation and charges collected +directly by the accommodation property upon check-in or check-out (e.g., resort fees, municipal +occupancy taxes, or a remaining room balance). #### Pricing Architecture & Scope Guidelines -* **Authoritative Root Total (`totals`)**: The top-level `totals` array represents the binding, authoritative pricing breakdown and aggregate financial commitment for the entire reservation stay across all requested room units. -* **Stay-Level Room Rate Total (`room_rates[].totals`)**: Each entry in `room_rates[].totals` reflects the total charges for that specific room rate unit across the entire itinerary stay duration (i.e., check-in to check-out), **NOT** a per-night figure. -* **Itemized Subtotals and Nightly Breakdown (`lines`)**: The `lines` array under a total item provides supplementary, itemized clarity: +* **Authoritative Root Total (`totals`)**: The top-level `totals` array represents the binding, + authoritative pricing breakdown and aggregate financial commitment for the entire reservation + stay across all requested room units. +* **Stay-Level Room Rate Total (`room_rates[].totals`)**: Each entry in `room_rates[].totals` + reflects the total charges for that specific room rate unit across the entire itinerary stay + duration (i.e., check-in to check-out), **NOT** a per-night figure. +* **Itemized Subtotals and Nightly Breakdown (`lines`)**: The `lines` array under a total item + provides supplementary, itemized clarity: * `subtotal` total items **MAY** carry `lines` representing the per-night room rate breakdown. - * `tax` total items **MAY** carry `lines` delineating separate tax authorities (e.g., state sales tax vs. local occupancy or tourism tax). - * `fee` total items **MAY** carry `lines` detailing mandatory charges (e.g., daily resort fees, cleaning fees). -* **Price Transparency and All-Inclusive Cost**: Platforms and Businesses **MUST** ensure that the guest is presented with the complete stay liability before booking confirmation. Hidden fees or undisclosed property charges violate price transparency standards. + * `tax` total items **MAY** carry `lines` delineating separate tax authorities (e.g., state + sales tax vs. local occupancy or tourism tax). + * `fee` total items **MAY** carry `lines` detailing mandatory charges (e.g., daily resort + fees, cleaning fees). +* **Price Transparency and All-Inclusive Cost**: Platforms and Businesses **MUST** ensure that + the guest is presented with the complete stay liability before booking confirmation. Hidden + fees or undisclosed property charges violate price transparency standards. #### Immediate vs. Deferred Payment Breakdown -* **Amount Due Now (`total`)**: The standard `type: "total"` entry strictly represents the immediate amount charged to the buyer's payment instrument upon booking confirmation. In full prepayment terms, this equals the entire cost of the stay. In deposit-based terms, this equals only the initial deposit and prepaid taxes/fees. The sum of all standard prepaid items (`subtotal` + prepaid `fee` + prepaid `tax` + `discount`) **MUST** equal `total`. -* **Deferred & Property-Collected Well-Known Types**: To model post-booking and property-collected charges consistently, the following well-known `type` values are introduced: - * `postpaid_subtotal`: The lodging room rate balance collected directly at the property (e.g., remaining room nights due at check-in after a partial deposit). - * `postpaid_fee`: Mandatory amenity, resort, cleaning, or facility fees collected directly by the property during the stay. - * `postpaid_tax`: Mandatory municipal, occupancy, or tourism taxes collected locally by the property (e.g., city accommodation tax). - * `due_at_property`: The aggregate sum of all property-collected charges (`postpaid_subtotal` + `postpaid_fee` + `postpaid_tax`). Present whenever any postpaid charges exist. - * `grand_total`: The all-inclusive stay cost (`total` + `due_at_property`), representing the Buyer's total financial commitment for the entire reservation. Present whenever deferred or property-collected amounts exist, ensuring full compliance with FTC and EU price display requirements. - * `grand_total`: The all-inclusive stay cost (`total` + `due_at_property`), representing the +* **Amount Due Now (`total`)**: The standard `type: "total"` entry strictly represents the + immediate amount charged to the buyer's payment instrument upon booking confirmation. In full + prepayment terms, this equals the entire cost of the stay. In deposit-based terms, this equals + only the initial deposit and prepaid taxes/fees. The sum of all standard prepaid items + (`subtotal`, `fee`, `tax`, `discount`) **MUST** equal `total`. +* **Deferred & Property-Collected Well-Known Types**: To model post-booking and + property-collected charges consistently, the following well-known `type` values are introduced: + * `postpaid_subtotal`: The lodging room rate balance collected directly at the property + (e.g., remaining room nights due at check-in after a partial deposit). + * `postpaid_fee`: Mandatory amenity, resort, cleaning, or facility fees collected directly + by the property during the stay. + * `postpaid_tax`: Mandatory municipal, occupancy, or tourism taxes collected locally by + the property (e.g., city accommodation tax). + * `due_at_property`: The aggregate sum of all property-collected charges + (`postpaid_subtotal` + `postpaid_fee` + `postpaid_tax`). Present whenever any postpaid charges exist. + * `grand_total`: The all-inclusive stay cost (`total` + `due_at_property`), representing + the Buyer's total financial commitment for the entire reservation. Present whenever deferred + or property-collected amounts exist, ensuring full compliance with FTC and EU price + display requirements. ##### Well-Known Totals Types & Accounting Invariants @@ -155,19 +182,31 @@ Lodging reservations follow strict all-in pricing rules to comply with consumer #### Local Tax & Fee Disclosures -When mandatory taxes or fees are collected locally by the lodging property and cannot be remitted at booking, the Business **SHOULD** provide a warning message in `messages[]` with `presentation: "disclosure"` and a `path` pointing directly to the relevant postpaid entry (e.g., `$.totals[4]`). This disclosure notice **MUST** state the applicable local rates, exemptions, and payment instructions, complemented by formal policy links in `links[]`. +When mandatory taxes or fees are collected locally by the lodging property and cannot be +remitted at booking, the Business **SHOULD** provide a warning message in `messages[]` with +`presentation: "disclosure"` and a `path` pointing directly to the relevant postpaid entry +(e.g., `$.totals[4]`). This disclosure notice **MUST** state the applicable local rates, +exemptions, and payment instructions, complemented by formal policy links in `links[]`. #### Payment Terms Integration (`dev.ucp.common.payment.terms`) -When a Business supports flexible payment timing, it advertises the `dev.ucp.common.payment.terms` capability and populates `payment.terms[]` with the available payment terms alongside `payment.selected_term_id` indicating the active selection: +When a Business supports flexible payment timing, it advertises the `dev.ucp.common.payment.terms` +capability and populates `payment.terms[]` with the available payment terms +alongside `payment.selected_term_id` indicating the active selection: * **Schedules and `totals` Alignment**: * A payment term is composed of one or more `schedules[]`. - * Schedules with `type: "immediate"` represent payments due today upon booking completion and **MUST** sum to `totals[].type: "total"`. - * Schedules with `type: "deferred"` represent payments due at a specified future date or event (e.g., `due_at` timestamp or check-in) and **MUST** sum to `totals[].type: "due_at_property"`. + * Schedules with `type: "immediate"` represent payments due today upon booking completion + and **MUST** sum to `totals[].type: "total"`. + * Schedules with `type: "deferred"` represent payments due at a specified future date or + event (e.g., `due_at` timestamp or check-in) and **MUST** sum to `totals[].type: "due_at_property"`. * **Selection Mutations**: - * When the Platform updates `payment.selected_term_id` via Update Booking Session (e.g., switching from "Pay now" to "First night now, balance at check-in"), the Business authoritatively recomputes `totals[]`. - * The recomputed `totals[]` reflects the newly selected term's immediate amount in `total`, partitions remaining nights into `postpaid_subtotal` and `postpaid_tax`, and updates `due_at_property` and `grand_total`. + * When the Platform updates `payment.selected_term_id` via Update Booking Session + (e.g., switching from "Pay now" to "First night now, balance at check-in"), + the Business authoritatively recomputes `totals[]`. + * The recomputed `totals[]` reflects the newly selected term's immediate amount in `total`, + partitions remaining nights into `postpaid_subtotal` and `postpaid_tax`, and updates + `due_at_property` and `grand_total`. The following snippets illustrate how `totals[]` is structured across three canonical lodging pricing patterns: @@ -284,7 +323,8 @@ The following snippets illustrate how `totals[]` is structured across three cano ``` > [!TIP] -> For complete, end-to-end booking session payloads with room rate bindings, lead guest assignments, messages, and payment terms, see [Pricing & Payment Terms Examples](#pricing-examples). +> For complete, end-to-end booking session payloads with room rate bindings, lead guest assignments, +> messages, and payment terms, see [Pricing & Payment Terms Examples](#pricing-examples). ### Payments @@ -294,9 +334,11 @@ collecting payment instruments (e.g., Google Pay, Shop Pay). When the user submits payment, the platform populates the `payment.instruments` array with the collected instrument data. -The `payment` object is optional on booking creation and may be omitted for -use cases that don't require immediate payment processing (e.g., pay after -arrival or hold-with-card). +The `payment` object is optional on booking session creation and update operations. +At completion (`complete_booking_session`), `payment` is **REQUIRED** to establish the binding +payment agreement. For immediate card payments, `payment.instruments` is populated; for deferred +or pay-at-property reservations, `payment` conveys the finalized terms (e.g., via the Payment +Terms extension) and `instruments` may be omitted if no upfront card guarantee is required. ### Booking Status Lifecycle @@ -420,6 +462,9 @@ Businesses **MUST** provide `continue_url` when returning `status` = for each entry in the root `guests[]` array (e.g., `"gst_01"`, `"gst_02"`). * **MUST** ensure every `guest_assignments[].guest_id` references a valid `id` present in the root `guests[]` pool. +* **SHOULD NOT** send `guests[]` personal identity fields beyond `id` before the + booking reaches `ready_for_complete`, and **SHOULD** send only the fields the + business requests via `messages[]`. * **MAY** engage an agent to facilitate the booking session (e.g. select room, dates, collect guest information). However, the agent must hand over the booking session to a trusted and deterministic UI for the user to review @@ -515,7 +560,8 @@ The abstract operations above are bound to specific transport protocols: ### Pricing Examples -The following examples provide complete, authoritative booking sessions demonstrating property-collected charges and flexible payment terms integration. +The following examples provide complete, authoritative booking sessions demonstrating +property-collected charges and flexible payment terms integration. === "Property-Collected Taxes & Fees" @@ -1082,6 +1128,14 @@ Payment details and collected payment instruments. {{ schema_fields('payment', 'lodging/booking') }} +### Policy + +Policies (cancellation terms, house rules, and the like) that apply to the booking session or room rates. +JSONPath targets in `applies_to` are relative to this response root (e.g., `$.room_rates[0]`). +See [Policies](../../overview/index.md#policies) for the full model. + +{{ schema_fields('types/policy', 'lodging/booking') }} + ### Rate Plan Commercial rate plan contract, cancellation policy rules, and rate inclusions. From a071fb60deff855241ac39f6ae2659ae74053760 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Sat, 5 Sep 2026 01:00:45 +0000 Subject: [PATCH 5/8] Minor example fix based on new normative guidelines. --- docs/specification/lodging/booking/mcp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specification/lodging/booking/mcp.md b/docs/specification/lodging/booking/mcp.md index 62dafa979..dc64021ce 100644 --- a/docs/specification/lodging/booking/mcp.md +++ b/docs/specification/lodging/booking/mcp.md @@ -210,7 +210,7 @@ Maps to the [Create Booking Session](index.md#create-booking-session) operation. } }, "id": "booking_123", - "status": "ready_for_complete", + "status": "incomplete", "accommodation": { "id": "hotel_123", "name": "Beautiful Scenery Hotel", From aa71276fc35b0ad9ae067d58cce5de4e44941f0b Mon Sep 17 00:00:00 2001 From: Jing Li Date: Tue, 8 Sep 2026 23:54:33 +0000 Subject: [PATCH 6/8] Address review feedback on totals modelling and warnings contract for totals_changed. --- docs/specification/lodging/booking/index.md | 105 +++++++++--------- source/schemas/common/types/warning_code.json | 1 + source/schemas/lodging/booking.json | 2 +- 3 files changed, 57 insertions(+), 51 deletions(-) diff --git a/docs/specification/lodging/booking/index.md b/docs/specification/lodging/booking/index.md index 367d20e02..476ef2953 100644 --- a/docs/specification/lodging/booking/index.md +++ b/docs/specification/lodging/booking/index.md @@ -145,13 +145,18 @@ occupancy taxes, or a remaining room balance). #### Immediate vs. Deferred Payment Breakdown -* **Amount Due Now (`total`)**: The standard `type: "total"` entry strictly represents the - immediate amount charged to the buyer's payment instrument upon booking confirmation. In full - prepayment terms, this equals the entire cost of the stay. In deposit-based terms, this equals - only the initial deposit and prepaid taxes/fees. The sum of all standard prepaid items - (`subtotal`, `fee`, `tax`, `discount`) **MUST** equal `total`. -* **Deferred & Property-Collected Well-Known Types**: To model post-booking and - property-collected charges consistently, the following well-known `type` values are introduced: +* **Authoritative Stay Liability (`total`)**: In accordance with the core `totals.json` + contract, the standard `type: "total"` entry strictly represents the authoritative all-in stay liability + (immediate charges plus deferred or property-collected amounts) for the entire reservation. + Every booking session **MUST** contain exactly one `total` entry, ensuring that Platforms display the full, + transparent cost of the stay upfront in compliance with FTC and EU price display directives. +* **Payment Timing Breakdown Types**: To clearly distinguish amounts charged immediately upon booking + confirmation from amounts collected later or at the property, the following well-known `type` values + are introduced: + * `due_now`: The immediate amount charged to the Buyer's payment instrument upon booking confirmation. + In full prepayment terms, `due_now` equals `total` (and **MAY** be omitted when no deferred balance exists). + In deposit-based or pay-at-property terms, `due_now` represents the upfront deposit and prepaid fees/taxes. + The sum of all prepaid items (`subtotal`, `fee`, `tax`, `discount`) **MUST** equal `due_now`. * `postpaid_subtotal`: The lodging room rate balance collected directly at the property (e.g., remaining room nights due at check-in after a partial deposit). * `postpaid_fee`: Mandatory amenity, resort, cleaning, or facility fees collected directly @@ -160,10 +165,6 @@ occupancy taxes, or a remaining room balance). the property (e.g., city accommodation tax). * `due_at_property`: The aggregate sum of all property-collected charges (`postpaid_subtotal` + `postpaid_fee` + `postpaid_tax`). Present whenever any postpaid charges exist. - * `grand_total`: The all-inclusive stay cost (`total` + `due_at_property`), representing - the Buyer's total financial commitment for the entire reservation. Present whenever deferred - or property-collected amounts exist, ensuring full compliance with FTC and EU price - display requirements. ##### Well-Known Totals Types & Accounting Invariants @@ -173,12 +174,12 @@ occupancy taxes, or a remaining room balance). | `fee` | Immediate (Due Now) | Prepaid service, processing, or booking fees | Optional, repeatable | | `tax` | Immediate (Due Now) | Prepaid state, value-added, or sales taxes | Optional, repeatable | | `discount` | Immediate (Due Now) | Rate reductions, promotional discounts, or pay-now savings | Optional, negative amount | -| `total` | Immediate (Due Now) | Aggregate amount charged upon confirmation | Exactly one entry in `totals[]`; equals sum of immediate group | +| `due_now` | Immediate (Due Now) | Aggregate amount charged upon confirmation | Requires `display_text`; equals sum of immediate group (`subtotal + fee + tax + discount`) | | `postpaid_subtotal` | Deferred (Property) | Remaining room rate balance collected at hotel | Requires `display_text` | | `postpaid_fee` | Deferred (Property) | Mandatory resort, facility, or cleaning fees paid at hotel | Requires `display_text` | | `postpaid_tax` | Deferred (Property) | Municipal, city, or occupancy taxes paid at hotel | Requires `display_text` | | `due_at_property` | Deferred (Property) | Aggregate total collected upon arrival/departure | Requires `display_text`; equals sum of deferred group | -| `grand_total` | Summary | Total stay liability across all payment timings | Requires `display_text`; equals `total` + `due_at_property` | +| `total` | Summary | Authoritative all-in stay liability across all payment timings | Exactly one entry in `totals[]`; equals `due_now` + `due_at_property` | #### Local Tax & Fee Disclosures @@ -196,23 +197,25 @@ alongside `payment.selected_term_id` indicating the active selection: * **Schedules and `totals` Alignment**: * A payment term is composed of one or more `schedules[]`. + * The sum of all `schedules[].amount` within a term **MUST** equal `totals[].type: "total"` + (satisfying the core `common/payment_terms.json` invariant). * Schedules with `type: "immediate"` represent payments due today upon booking completion - and **MUST** sum to `totals[].type: "total"`. + and **MUST** sum to `totals[].type: "due_now"`. * Schedules with `type: "deferred"` represent payments due at a specified future date or event (e.g., `due_at` timestamp or check-in) and **MUST** sum to `totals[].type: "due_at_property"`. * **Selection Mutations**: * When the Platform updates `payment.selected_term_id` via Update Booking Session (e.g., switching from "Pay now" to "First night now, balance at check-in"), the Business authoritatively recomputes `totals[]`. - * The recomputed `totals[]` reflects the newly selected term's immediate amount in `total`, - partitions remaining nights into `postpaid_subtotal` and `postpaid_tax`, and updates - `due_at_property` and `grand_total`. + * The recomputed `totals[]` reflects the newly selected term's immediate amount in `due_now`, + partitions remaining nights into `postpaid_subtotal` and `postpaid_tax`, updates + `due_at_property`, and updates `total` to reflect the all-in stay liability under that term. The following snippets illustrate how `totals[]` is structured across three canonical lodging pricing patterns: === "Pattern 1: Property-Collected Charges" - Prepaid room rate and service fee charged immediately (`total: 70400`), while resort fee and Tokyo Accommodation Tax are collected at check-in (`due_at_property: 6000`), totaling `grand_total: 76400`: + Prepaid room rate and service fee charged immediately (`due_now: 70400`), while resort fee and Tokyo Accommodation Tax are collected at check-in (`due_at_property: 6000`), totaling `total: 76400`: ```json @@ -228,7 +231,7 @@ The following snippets illustrate how `totals[]` is structured across three cano "amount": 6400 }, { - "type": "total", + "type": "due_now", "display_text": "Total Due Now (Charged Today)", "amount": 70400 }, @@ -248,8 +251,8 @@ The following snippets illustrate how `totals[]` is structured across three cano "amount": 6000 }, { - "type": "grand_total", - "display_text": "Grand Total (Total Stay Cost)", + "type": "total", + "display_text": "Total Stay Cost", "amount": 76400 } ] @@ -257,7 +260,7 @@ The following snippets illustrate how `totals[]` is structured across three cano === "Pattern 2: Upfront Payment with Savings" - Upfront payment term selected (`pt_pay_now`), offering a $50 discount (`discount: -5000`) for paying in full today. No deferred balance remains (`due_at_property: 0`): + Upfront payment term selected (`pt_pay_now`), offering a $50 discount (`discount: -5000`) for paying in full today. The upfront charge (`due_now: 130000`) covers the entire reservation liability (`total: 130000`), with no deferred balance remaining (`due_at_property: 0`): ```json @@ -266,15 +269,15 @@ The following snippets illustrate how `totals[]` is structured across three cano { "type": "fee", "display_text": "Service Fee", "amount": 3000 }, { "type": "tax", "display_text": "State Lodging Tax (10%)", "amount": 12000 }, { "type": "discount", "display_text": "Pay-now saving", "amount": -5000 }, - { "type": "total", "display_text": "Total Due Now (Charged Today)", "amount": 130000 }, + { "type": "due_now", "display_text": "Total Due Now (Charged Today)", "amount": 130000 }, { "type": "due_at_property", "display_text": "Total Due at Property", "amount": 0 }, - { "type": "grand_total", "display_text": "Grand Total (Total Stay Cost)", "amount": 130000 } + { "type": "total", "display_text": "Total Stay Cost", "amount": 130000 } ] ``` === "Pattern 3: Deposit & Check-in Balance" - Deposit term selected (`pt_deposit_balance`), charging the 1st night deposit and initial taxes/fees today (`total: 47000`), with the remaining 2 nights rate and tax collected at check-in (`due_at_property: 88000`), yielding `grand_total: 135000`: + Deposit term selected (`pt_deposit_balance`), charging the 1st night deposit and initial taxes/fees today (`due_now: 47000`), with the remaining 2 nights rate and tax collected at check-in (`due_at_property: 88000`), yielding total stay liability `total: 135000`: ```json @@ -295,7 +298,7 @@ The following snippets illustrate how `totals[]` is structured across three cano "amount": 4000 }, { - "type": "total", + "type": "due_now", "display_text": "Total Due Now (Deposit Charged Today)", "amount": 47000 }, @@ -315,8 +318,8 @@ The following snippets illustrate how `totals[]` is structured across three cano "amount": 88000 }, { - "type": "grand_total", - "display_text": "Grand Total (Total Stay Cost)", + "type": "total", + "display_text": "Total Stay Cost", "amount": 135000 } ] @@ -412,11 +415,12 @@ Both result in user handoff, but represent different booking session states: #### Standard Errors -| Code | Description | -| :-------------------- | :--------------------------------------------------------- | -| `inventory_exhausted` | The selected room or inventory hold is no longer available | -| `payment_failed` | Payment processing failed | -| `eligibility_invalid` | Eligibility claim could not be verified at completion | +| Code | Description | +| :---------------------------- | :-------------------------------------------------------------- | +| `inventory_exhausted` | The selected room or inventory hold is no longer available | +| `occupancy_exceeded_capacity` | Number of assigned guests exceeds physical room capacity bounds | +| `payment_failed` | Payment processing failed | +| `eligibility_invalid` | Eligibility claim could not be verified at completion | ### Warning Presentation @@ -424,16 +428,17 @@ The `presentation` field on warning messages controls the rendering contract the platform **MUST** follow (e.g., `notice` vs. `disclosure`). For the authoritative rendering rules, see [Checkout — Warning Presentation](../../shopping/checkout/index.md#warning-presentation). -#### Price Changes +#### Totals Changes -When inventory rates or commercial pricing terms fluctuate during an active booking -session, the Business informs the Platform using a standard warning with -`code: "price_changed"` and `severity: "recoverable"`. +When room rates, taxes, or mandatory fees fluctuate during an active booking +session, the Business returns the updated session with recomputed `totals[]` and +**MUST** report the modification using a warning message in `messages[]` with +`code: "totals_changed"` and `path: "$.totals"`. -The Business **MUST NOT** complete a booking session if unacknowledged price -changes occur. When a `price_changed` warning is received, the Platform **MUST** -present the updated totals to the buyer for explicit confirmation before submitting -a Complete Booking Session request. +The Business **MUST** set `presentation: "disclosure"` when the revised amount +requires prominent Buyer awareness. The Platform **MUST** present the updated +totals and the warning content to the buyer, and **MUST NOT** auto-dismiss or hide +the disclosure. ## Continue URL @@ -643,7 +648,7 @@ property-collected charges and flexible payment terms integration. "amount": 6400 }, { - "type": "total", + "type": "due_now", "display_text": "Total Due Now (Charged Today)", "amount": 70400 }, @@ -663,8 +668,8 @@ property-collected charges and flexible payment terms integration. "amount": 6000 }, { - "type": "grand_total", - "display_text": "Grand Total (Total Stay Cost)", + "type": "total", + "display_text": "Total Stay Cost", "amount": 76400 } ], @@ -794,7 +799,7 @@ property-collected charges and flexible payment terms integration. "amount": -5000 }, { - "type": "total", + "type": "due_now", "display_text": "Total Due Now (Charged Today)", "amount": 130000 }, @@ -804,8 +809,8 @@ property-collected charges and flexible payment terms integration. "amount": 0 }, { - "type": "grand_total", - "display_text": "Grand Total (Total Stay Cost)", + "type": "total", + "display_text": "Total Stay Cost", "amount": 130000 } ], @@ -957,7 +962,7 @@ property-collected charges and flexible payment terms integration. "amount": 4000 }, { - "type": "total", + "type": "due_now", "display_text": "Total Due Now (Deposit Charged Today)", "amount": 47000 }, @@ -977,8 +982,8 @@ property-collected charges and flexible payment terms integration. "amount": 88000 }, { - "type": "grand_total", - "display_text": "Grand Total (Total Stay Cost)", + "type": "total", + "display_text": "Total Stay Cost", "amount": 135000 } ], diff --git a/source/schemas/common/types/warning_code.json b/source/schemas/common/types/warning_code.json index 59071de89..abeb26352 100644 --- a/source/schemas/common/types/warning_code.json +++ b/source/schemas/common/types/warning_code.json @@ -9,6 +9,7 @@ "prop65", "fulfillment_changed", "payment_term_changed", + "totals_changed", "age_restricted" ] } diff --git a/source/schemas/lodging/booking.json b/source/schemas/lodging/booking.json index 18f779243..1a41c53d0 100644 --- a/source/schemas/lodging/booking.json +++ b/source/schemas/lodging/booking.json @@ -127,7 +127,7 @@ }, "totals": { "$ref": "../common/types/totals.json", - "description": "Authoritative pricing breakdown for the booking. The `total` entry represents the immediate amount payable upon confirmation (Due Now). When property-collected or deferred charges apply, businesses provide `postpaid_*` entries, `due_at_property`, and `grand_total` to convey the all-inclusive stay liability in compliance with price transparency directives.", + "description": "Authoritative pricing breakdown for the booking. The `total` entry represents the authoritative all-in stay liability (immediate + deferred). When payment timing is split or property-collected charges apply, businesses provide `due_now` (immediate charge upon confirmation), `postpaid_*` entries, and `due_at_property` (aggregate property-collected charges) to convey timing breakdowns while maintaining full price transparency.", "ucp_request": "omit" }, "actions": { From a3b2d0cdc5763203ec3df4bb1fb30a35e6561de9 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Wed, 9 Sep 2026 15:46:18 +0000 Subject: [PATCH 7/8] Address minor feedback on scaffold and guest's session-scoped identifier. --- docs/specification/lodging/booking/index.md | 4 ++-- docs/specification/lodging/booking/mcp.md | 2 +- scripts/scaffolds/lodging_booking_request_create.json | 2 +- scripts/scaffolds/lodging_booking_request_update.json | 2 +- scripts/scaffolds/lodging_booking_response.json | 2 +- source/schemas/lodging/types/guest.json | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/specification/lodging/booking/index.md b/docs/specification/lodging/booking/index.md index 476ef2953..e92892f80 100644 --- a/docs/specification/lodging/booking/index.md +++ b/docs/specification/lodging/booking/index.md @@ -463,8 +463,8 @@ Businesses **MUST** provide `continue_url` when returning `status` = * **MUST** identify a lead guest by providing `booker` details or designating at least one guest with `role: "primary_guest"` (including full legal name and contact details) prior to invoking Complete Booking Session. -* **MUST** generate unique, stable string identifiers in the Platform namespace - for each entry in the root `guests[]` array (e.g., `"gst_01"`, `"gst_02"`). +* **MUST** generate unique, stable, session-scoped string identifiers in the Platform + namespace for each entry in the root `guests[]` array (e.g., `"gst_01"`, `"gst_02"`). * **MUST** ensure every `guest_assignments[].guest_id` references a valid `id` present in the root `guests[]` pool. * **SHOULD NOT** send `guests[]` personal identity fields beyond `id` before the diff --git a/docs/specification/lodging/booking/mcp.md b/docs/specification/lodging/booking/mcp.md index dc64021ce..9c63f157d 100644 --- a/docs/specification/lodging/booking/mcp.md +++ b/docs/specification/lodging/booking/mcp.md @@ -74,7 +74,7 @@ Businesses advertise MCP transport availability through their UCP profile at MCP clients **MUST** include a `meta` object in every request containing protocol metadata: - + ```json { "jsonrpc": "2.0", diff --git a/scripts/scaffolds/lodging_booking_request_create.json b/scripts/scaffolds/lodging_booking_request_create.json index 50dc2baf3..d7cdbf450 100644 --- a/scripts/scaffolds/lodging_booking_request_create.json +++ b/scripts/scaffolds/lodging_booking_request_create.json @@ -5,7 +5,7 @@ "room_rates": [ { "id": "rr_scaffold", - "room_details": { + "room_type": { "id": "room_scaffold" }, "rate_plan": { diff --git a/scripts/scaffolds/lodging_booking_request_update.json b/scripts/scaffolds/lodging_booking_request_update.json index a87562ba9..4a40c8587 100644 --- a/scripts/scaffolds/lodging_booking_request_update.json +++ b/scripts/scaffolds/lodging_booking_request_update.json @@ -5,7 +5,7 @@ "room_rates": [ { "id": "rr_scaffold", - "room_details": { + "room_type": { "id": "room_scaffold" }, "rate_plan": { diff --git a/scripts/scaffolds/lodging_booking_response.json b/scripts/scaffolds/lodging_booking_response.json index a9c7df04d..199b7d576 100644 --- a/scripts/scaffolds/lodging_booking_response.json +++ b/scripts/scaffolds/lodging_booking_response.json @@ -25,7 +25,7 @@ "room_rates": [ { "id": "rr_scaffold", - "room_details": { + "room_type": { "id": "room_scaffold", "title": "Scaffold Room", "capacity": { diff --git a/source/schemas/lodging/types/guest.json b/source/schemas/lodging/types/guest.json index 070f75841..9175c61bf 100644 --- a/source/schemas/lodging/types/guest.json +++ b/source/schemas/lodging/types/guest.json @@ -11,7 +11,7 @@ "properties": { "id": { "type": "string", - "description": "Stable, opaque identifier for a guest in the Platform's namespace." + "description": "Stable, opaque session-scoped identifier for a guest in the Platform's namespace." }, "first_name": { "type": "string", From 8d6c7d97b79b49b319c4899423f0f3614844ac66 Mon Sep 17 00:00:00 2001 From: Jing Li Date: Thu, 10 Sep 2026 15:59:48 +0000 Subject: [PATCH 8/8] Address some wording suggestion updates. --- source/schemas/lodging/types/booking_confirmation.json | 1 + source/schemas/lodging/types/capacity.json | 8 ++++---- source/schemas/lodging/types/occupancy.json | 4 ++-- source/schemas/lodging/types/room_rate.json | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/source/schemas/lodging/types/booking_confirmation.json b/source/schemas/lodging/types/booking_confirmation.json index cc9618e8c..6c4723c5d 100644 --- a/source/schemas/lodging/types/booking_confirmation.json +++ b/source/schemas/lodging/types/booking_confirmation.json @@ -4,6 +4,7 @@ "title": "Booking Confirmation", "description": "Booking details available at the time of session completion.", "type": "object", + "additionalProperties": true, "required": [ "id" ], diff --git a/source/schemas/lodging/types/capacity.json b/source/schemas/lodging/types/capacity.json index 692927a86..99e0386fd 100644 --- a/source/schemas/lodging/types/capacity.json +++ b/source/schemas/lodging/types/capacity.json @@ -2,13 +2,13 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/lodging/types/capacity.json", "title": "Capacity", - "description": "Physical room capacity and allowed guest age rules.", + "description": "Physical room capacity specifying maximum occupancy limits broken down by guest age.", "type": "object", "additionalProperties": true, "properties": { "adults": { "type": "integer", - "description": "Number of adults.", + "description": "Maximum number of adult occupants.", "minimum": 0 }, "children": { @@ -33,11 +33,11 @@ } } }, - "description": "Child capacity limits broken down by age brackets." + "description": "Maximum number of child occupants broken down by age brackets." }, "total": { "type": "integer", - "description": "Total number of occupants. MUST be equal to adults + sum(children.total).", + "description": "Maximum number of total occupants (adult or child). MUST be equal to adults + sum(children.total).", "minimum": 0 } } diff --git a/source/schemas/lodging/types/occupancy.json b/source/schemas/lodging/types/occupancy.json index 3db53c3b1..2c9621f31 100644 --- a/source/schemas/lodging/types/occupancy.json +++ b/source/schemas/lodging/types/occupancy.json @@ -26,12 +26,12 @@ "minimum": 0, "maximum": 17 }, - "description": "Ages of children occupying the room." + "description": "Ages of children occupying the room. If present length(child_ages) MUST equal children." }, "total": { "type": "integer", "minimum": 1, - "description": "Total number of occupants in the room." + "description": "Total number of occupants in the room. If adults and/or children fields are specified, this MUST equal adults + children." } } } diff --git a/source/schemas/lodging/types/room_rate.json b/source/schemas/lodging/types/room_rate.json index adeeb9867..5a97ff611 100644 --- a/source/schemas/lodging/types/room_rate.json +++ b/source/schemas/lodging/types/room_rate.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/lodging/types/room_rate.json", "title": "Room Rate", - "description": "Compound binding schema linking a physical room type, commercial rate plan, occupancy, guest assignments, optional add-ons, and pricing totals.", + "description": "Compound binding schema linking a physical room type, commercial rate plan, occupancy, guest assignments, and pricing totals.", "type": "object", "required": [ "id"