Skip to content

feat: Introduce Lodging Booking Capability and Cancellation Policy Extension - #780

Open
jingyli wants to merge 12 commits into
mainfrom
lodging/booking
Open

feat: Introduce Lodging Booking Capability and Cancellation Policy Extension#780
jingyli wants to merge 12 commits into
mainfrom
lodging/booking

Conversation

@jingyli

@jingyli jingyli commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Description

On behalf of the entire Lodging Domain Tech Council, this PR introduces the Lodging Booking Capability (dev.ucp.lodging.booking) and the Cancellation Policy Extension (dev.ucp.lodging.policy.cancellation), establishing the foundational data contracts, state lifecycle, and transport bindings for lodging reservations in UCP.

By standardizing compound room-rate bindings, a decoupled guest pool assignment model, and machine-readable cancellation classifications across both REST and MCP transports, this capability enables platforms to facilitate hotel booking sessions seamlessly while leaving inventory authority, pricing calculations, and Merchant of Record (MoR) responsibilities with the business.

Motivation

While retail checkout (dev.ucp.shopping.checkout) models itemized physical/digital goods with simple quantities, lodging commerce introduces distinct transactional dynamics:

  1. Compound Offer Binding (room_rate): A bookable lodging unit is not a simple SKU; it is a compound binding of a physical room real-estate unit (room_details), a commercial rate plan contract (rate_plan), a date interval (itinerary), and an occupancy configuration (occupancy).
  2. Buyer vs. Occupant Separation: The legal contracting and paying party (booker) frequently differs from the individuals physically occupying the rooms (guests), especially in corporate travel, proxy bookings, family reservations, and multi-room bookings.
  3. Upper-Funnel to Transaction Continuity: Room rates and property availability discovered upstream during search must be ingested cleanly into booking sessions, where real-time inventory and pricing are validated and expanded authoritatively.
  4. Pre-Purchase Cancellation Transparency: Cancellation terms vary widely across rate plans. Providing standardized machine-readable refundability classifications alongside human-readable deadline schedules allows platforms to answer buyer questions ("Is this refundable?", "What is the penalty cutoff?") deterministically without scraping or parsing external web pages.

Proposal Scope

This PR focuses on the core pre-purchase through booking completion lifecycle for lodging reservations.

In Scope

  • Core Booking Capability (dev.ucp.lodging.booking): Full session lifecycle (incomplete, requires_escalation, ready_for_complete, complete_in_progress, completed, canceled) with deterministic handoffs via continue_url.
  • Data Models & Types:
    • Shared date_interval primitive in common/types/.
    • Lodging entities: accommodation, room_rate, room_details, rate_plan, occupancy, capacity, booker, guest, guest_assignment, and booking_confirmation.
  • Cancellation Policy Extension (dev.ucp.lodging.policy.cancellation): Pre-purchase cancellation terms decorating the core policies[] primitive with tri-state refundability (refundable, partially_refundable, non_refundable).
  • Transport Bindings: Complete REST (OpenAPI 3.1) and MCP (OpenRPC / tool definitions) specifications and examples.
  • Payment Extension Interoperability: Wiring of common payment capabilities (payment_terms, payment_split_payments, payment_authentication, payment_ap2_mandate) onto dev.ucp.lodging.booking.
  • Documentation & Tooling Enhancements: Enhanced main.py schema macro renderer to cleanly deduplicate overridden fields in allOf compositions, plus test scaffolds for lodging contracts.

Out of Scope

  • Upstream lodging search/discovery service.
  • Post-purchase reservation management (modifications, cancellations, and check-in workflows), which will be addressed in post-purchase order extensions.

Design Details

1. Compound Room Rate & Upstream Discovery

A reservation contains one or more room_rates[] items. Each item binds:

  • room_details.id: The physical room category (e.g. rt_luxury_queen).
  • rate_plan.id: The commercial rate terms and inclusions (e.g. rp_avg_base_rate).
  • occupancy: Adult and child count for the room.
  • guest_assignments: Specific guest mappings for the room.
{
  "accommodation": { "id": "hotel_123" },
  "room_rates": [
    {
      "id": "rt_luxury_queen__rp_avg_base_rate",
      "room_details": { "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"
  }
}

When creating a session, the Platform provides the minimal discovery keys. The Business authoritatively validates availability and expands the response with titles, descriptions, capacity limits, calculated price breakdowns (totals[]), and policy terms (policies[]).

2. Platform-Generated Guest IDs & Two-Tier Guest Pool

To eliminate ID remapping across distributed systems and avoid transmitting unnecessary PII before booking, guest data is structured into a two-level relational model:

  • Root Guest Pool (guests[]): A flat array of individual guest profiles. Each guest.id (e.g., "gst_01") is generated and supplied by the Platform in the platform namespace.
  • Room Assignments (room_rates[].guest_assignments[]): Granular mappings referencing guest.id and designating roles (primary_guest, additional_guest).
  • booker vs. guests: Decouples the legal purchaser from the room occupants.
{
  "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"
    }
  ],
  "room_rates": [
    {
      "id": "rt_luxury_queen__rp_avg_base_rate",
      "guest_assignments": [
        { "guest_id": "gst_01", "role": "primary_guest" },
        { "guest_id": "gst_02", "role": "additional_guest" }
      ]
    }
  ]
}

3. Cancellation Policy Extension (dev.ucp.lodging.policy.cancellation)

Extends the core policies[] primitive with machine-readable terms:

  • refundability: Tri-state classification (refundable, partially_refundable, non_refundable).
  • description: Explicit human-readable penalty schedules, cutoff times, and financial effects.
  • url: Direct link to the property's legal policy document.
{
  "policies": [
    {
      "type": "dev.ucp.lodging.policy.cancellation",
      "refundability": "partially_refundable",
      "description": {
        "text": "Cancel before July 10 for full refund. Cancellations between July 10 and July 14 incur a 1-night penalty fee."
      },
      "url": "https://business.example.com/cancellation"
    }
  ]
}

4. Documentation Engine Enhancement (main.py)

When generating schema reference tables (auto_generate_schema_reference, extension_schema_fields), allOf compositions that specialized a base field (such as type in policy_cancellation.json) previously generated duplicate rows for the same field.

_render_embedded_table and _render_table_from_schema in main.py were updated to deduplicate rows by field name and prefer the outermost/specialized definition, ensuring generated markdown tables display a single, authoritative row per field.

Category (Required)

Please select one or more categories that apply to this change.

  • Core Protocol: Changes to the base communication layer, global context, or breaking refactors. (Requires Technical Council approval)
  • Governance/Contributing: Updates to GOVERNANCE.md, CONTRIBUTING.md, or CODEOWNERS. (Requires Governance Council approval)
  • Capability: New schemas (Discovery, Cart, etc.) or extensions. (Requires Maintainer approval)
  • Documentation: Updates to README, or documentations regarding schema or capabilities. (Requires Maintainer approval)
  • Infrastructure: CI/CD, Linters, or build scripts. (Requires DevOps Maintainer approval)
  • Maintenance: Version bumps, lockfile updates, or minor bug fixes. (Requires DevOps Maintainer approval)
  • SDK: Language-specific SDK updates and releases. (Requires DevOps Maintainer approval)
  • Samples / Conformance: Maintaining samples and the conformance suite. (Requires Maintainer approval)
  • UCP Schema: Changes to the ucp-schema tool (resolver, linter, validator). (Requires Maintainer approval)
  • Community Health (.github): Updates to templates, workflows, or org-level configs. (Requires DevOps Maintainer approval)

Related Issues

#543

Checklist

  • I have followed the Contributing Guide (including Conventional Commits title requirements and ! for breaking changes).
  • I have updated the documentation (if applicable).
  • My changes pass all local linting and formatting checks.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • (For Core/Capability) I have included/updated the relevant JSON schemas.
  • I have regenerated Python Pydantic models by running generate_models.sh under python_sdk.

Screenshots / Logs (if applicable)

lodging-booking

@jingyli jingyli changed the title feat!: Introduce Lodging Booking Capability and Cancellation Policy Extension feat: Introduce Lodging Booking Capability and Cancellation Policy Extension Aug 28, 2026
@damaz91 damaz91 added the status:needs-triage Signal that the PR is ready for human triage label Aug 28, 2026
Comment thread source/schemas/lodging/policy_cancellation.json Outdated
Comment thread main.py
Comment thread source/schemas/lodging/booking.json
Comment thread docs/specification/lodging/booking/index.md Outdated
Comment thread docs/specification/lodging/booking/index.md Outdated
Comment thread source/schemas/lodging/policy_cancellation.json Outdated
Comment thread source/schemas/lodging/policy_cancellation.json
Comment thread source/schemas/common/types/date_interval.json Outdated
Comment thread source/schemas/common/types/date_interval.json
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[]`).

@amithanda amithanda Aug 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the relationship between room_rates[].totals and the root totals?

Nothing in room_rate.json, booking.json, or index.md defines the scope of a per-room-rate totals block. Both use common/types/totals.json, whose description says "MUST contain exactly one subtotal and one total entry" and whose rendering contract says platforms MUST render all entries. A platform sees two structurally identical, equally authoritative price blocks with no rule for which to show.

The examples do not disambiguate it and do not reconcile. In every response example across both bindings (rest.md:208/229, 454/504, 662/712, 845/895, 1028/1078, and the mcp.md equivalents):

itinerary:            2026-07-15 -> 2026-07-21   (6 nights)
room_rates[0].totals: subtotal 55000, tax 5500, total 60500
root totals:          subtotal 385000, tax 38500, fee 1000, total 424500

There is exactly one room rate. If room_rate.totals covered the stay, the root subtotal would be 55000. If it is a nightly rate, the root subtotal should be 6 x 55000 = 330000. It is 385000, which is 7 x 55000. Under either reading the flagship example is wrong, and the two readings differ by a factor of six in what a platform displays next to the room. Each block is internally consistent (10% tax, totals sum correctly), so this only shows up when you cross-check the two against the night count, which nothing automated does.

totals[].lines already exists for exactly this: "Optional itemized breakdown. The parent entry is always rendered; lines are supplementary. Sum of line amounts MUST equal the parent entry amount." Could we state the scope normatively and use lines for the nightly view?

Suggested change
authoritative cancellation terms (`policies[]`).
authoritative cancellation terms (`policies[]`).
### Pricing scope
`room_rates[].totals` states the price of that room rate for the **entire**
itinerary, not per night. The root `totals` is the authoritative booking total and
**MUST** equal the sum of all `room_rates[].totals` plus any booking-level fees and
taxes. Businesses **MAY** provide a per-night breakdown using `totals[].lines`,
which is supplementary and does not change the parent amount. Platforms **MUST**
render the root `total` as the price of the booking.

Which reading do we intend? Whichever it is, the example arithmetic needs a pass, since 385000 does not equal either 55000 or 6 x 55000.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this comment and one below (#780 (comment)), I realized the original specification documentation was not descriptive enough on the key pricing concept in lodging (and did not capture a lot of interesting DTC discussions and alignments around how terms should be modelled).

Enhanced the documentation as part of 87976f7 and provided some more concrete examples. PTAL!

Comment thread source/schemas/lodging/types/guest_assignment.json
Comment thread docs/specification/lodging/booking/rest.md Outdated

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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does payment being complete: "required" contradict the pay-at-property text here?

The annotation on booking.json:167-171 is {"create": "optional", "update": "optional", "complete": "required"}, and ucp-schema resolve --op complete --request returns required: ["payment"]. So payment is optional right up until the point where it becomes mandatory, and a genuine pay-at-property reservation with no card guarantee cannot be completed.

This is inherited verbatim from checkout.json, where it is correct, because checkout never claims payment is optional at completion. Lodging adds prose that contradicts the inherited annotation, so one of the two has to move. What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just sharing my naive thought on this front: I think payment being required in complete_booking_session remains the correct contract, even for the pay-at-property scenario where no instrument guarantee is needed.

This is because right now our modelling of payment_terms (where the deferred/pay-at-property schedule would be specified) is an extension that decorates the payment field. We need a final state to which the user locks in a selected payment term, regardless whether a payment token will be processed as part of the operation or not and enforcing the required payment construct in complete_booking_session makes the selection explicit (not relying on the latest selected term in an update call) while also keeping interface consistency/parity with lower-funnel checkout capabilities in retail shopping.

Comment thread docs/specification/lodging/booking/index.md
Comment thread docs/specification/lodging/booking/mcp.md Outdated
Comment thread docs/specification/lodging/booking/mcp.md Outdated
Comment thread docs/specification/lodging/booking/mcp.md Outdated
Comment thread docs/specification/lodging/booking/mcp.md Outdated
Comment thread docs/specification/lodging/booking/mcp.md Outdated
Comment thread mkdocs.yml Outdated
Comment thread source/schemas/lodging/policy_cancellation.json
Comment thread source/schemas/lodging/types/booker.json Outdated
Comment thread docs/specification/lodging/booking/index.md Outdated
Comment thread docs/specification/lodging/booking/index.md Outdated
"properties": {
"id": {
"type": "string",
"description": "Stable, opaque unique identifier of the room rate binding.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does a booking express two identical rooms?

room_rates[] has no quantity, and room_rate.id is a compound offer key (rt_luxury_queen__rp_avg_base_rate in every example) rather than a per-unit instance id. To book two Luxury Queen rooms on the same rate plan a platform emits two array entries with the same id:

"room_rates": [
  { "id": "rt_luxury_queen__rp_avg_base_rate", "guest_assignments": [{ "guest_id": "gst_01", "role": "primary_guest" }] },
  { "id": "rt_luxury_queen__rp_avg_base_rate", "guest_assignments": [{ "guest_id": "gst_03", "role": "primary_guest" }] }
]

applies_to: ["$.room_rates[0]"] is then positional, and since Update is a full-replacement PUT, a business that reorders the array in a response silently retargets a room-scoped cancellation policy to the other room. There is also no stable handle for "the second room" across updates.

A quantity field would conflict with per-room guest_assignments (you cannot assign different guests to each of two rooms collapsed into one entry with quantity: 2), so making the id a per-unit instance identifier seems cleaner. The offer key is already fully expressed by room_type.id + rate_plan.id.

Suggested change
"description": "Stable, opaque unique identifier of the room rate binding.",
"description": "Stable, opaque identifier for this room rate binding. Identifies one bookable unit: when a booking contains several rooms of the same type on the same rate plan, each MUST carry a distinct `id`. The commercial offer is identified by `room_type.id` + `rate_plan.id`, not by this field.",

The PR's motivation calls out multi-room bookings explicitly, so would a two-room example in rest.md be worth adding to settle it either way?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quantity vs Repeated Entry Modeling

We explicitly opted not to model quantity for RoomRate. It is more common in the lodging industry to represent multiple room bookings as separate entries, and as you've already noticed there are fields such as guest_assignments which can differ between entires.

ID Uniqueness

RoomRate.id, RoomType.id, and RatePlan.id are all identifiers that correspond to Business-specific concepts and should be retrieved from an upper-funnel discovery mechanism for that Business. Requiring uniqueness of these fields is therefore infeasible, as two entries for the same type of room and rate plan would have matching ids that are needed for Business lookups.

You may also note from the anyOf in RoomRate that RoomRate.id is not required if RatePlan.id and RoomType.id are both present, so targeting based on this field would be insuficient.

Policy targeting

As for handling applies_to targeting, it's a fair callout that relying on order/indexing has the potential to be error-prone if order is not preserved by the Business, however this is also explicitly listed as a supported mechanism in the Targeting spec and Policies Spec.

It would be my preference to maintain index based targeting and reinforce through documentation the importance of order persistence; however, I will present an alternative:

A Business may present a uid in the response for any schema with "additionalProperties": true and target that if they would prefer that to index based targeting. For example:

"room_rates": [
  { "uid": "room_rate_1", "id": "rt_luxury_queen__rp_avg_base_rate", "guest_assignments": [{ "guest_id": "gst_01", "role": "primary_guest" }] },
  { "uid": "room_rate_2", "id": "rt_luxury_queen__rp_avg_base_rate", "guest_assignments": [{ "guest_id": "gst_03", "role": "primary_guest" }] }
]

Which could then be targeted with applies_to: ["$.room_rates[?@.uid=='room_rate_1']"] to target a specific room_rates[] entry or applies_to: ["$.room_rates[?@.id=='rt_luxury_queen__rp_avg_base_rate']"] to target all RoomRates of that id/type.

@yairsabag

Copy link
Copy Markdown

Great to see the Lodging TC defining cancellation directly on the Booking surface. I agree with keeping the first version focused, but there is one gap worth resolving before the wire shape settles.

The PR says the extension enables platforms to answer questions such as "What is the penalty cutoff?" deterministically. At present, however, the schema structures only the current refundability classification. The cutoff, timezone, sequence of penalty windows, financial outcome, and no-show consequence remain exclusively in description.

For example, the included "free until Dec 20; one-night penalty thereafter" policy still requires a platform to parse prose before it can determine either the applicable deadline or the amount the buyer receives back. refundability is also time-dependent, but the structured object does not state the anchor, evaluation instant, or transition schedule.

I raised this tiered, anchor-relative policy class while policies[] was being designed in #572, where it was confirmed as an appropriate policy extension. I subsequently validated the shape against approximately 40 real cancellation policies across lodging, ticketing, and services, and published an executable schema, evaluator, and test vectors:

The corpus showed that the recurring deterministic core is an anchor plus ordered tiers, with outcomes expressed as percentages, fixed fees, or unit deductions such as "one night." It also covers no-show outcomes, which are particularly relevant to lodging.

I do not think this PR necessarily needs to absorb the full model immediately. Two incremental options seem possible:

  1. Keep refundability as a classification-only v1 and explicitly track structured deadlines and penalties as a follow-up; or
  2. Add an optional structured schedule alongside refundability, while retaining description as the universal human-readable fallback.

I would be happy to contribute a focused patch or adapt the existing test vectors to the Lodging TC's preferred shape.

@jingyli
jingyli requested a review from amithanda September 9, 2026 00:21
Comment thread docs/specification/lodging/booking/mcp.md Outdated
Comment thread source/schemas/lodging/types/guest.json Outdated
Comment thread scripts/scaffolds/lodging_booking_request_update.json Outdated
Comment thread scripts/scaffolds/lodging_booking_request_create.json Outdated
Comment thread scripts/scaffolds/lodging_booking_response.json Outdated

@amithanda amithanda left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some minor documentation clean up suggestions. PTAL.

@juliekye

juliekye commented Sep 9, 2026

Copy link
Copy Markdown

Coexistence with checkout-based lodging implementations

We're a hotel company that has built a UCP integration representing hotel bookings on the generic checkout capability (lodging details carried as custom fields on checkout sessions), developed against a major agent platform's integration requirements and currently in pre-production. With dev.ucp.lodging.booking landing, two questions:

  1. Can a business's /.well-known/ucp manifest declare both the checkout capability and the lodging booking capability simultaneously during a transition period? If so, is there guidance on operating both against the same inventory — shared vs. independent session scope, idempotency, and status semantics?
  2. Is the Lodging Domain Tech Council coordinating adoption timelines across agent platforms, so merchants converge on this capability rather than maintaining a divergent checkout-based lodging shape per platform?

Even a short non-normative "transitioning from checkout-based lodging" note in the docs would help early implementers plan the rebuild with confidence about what's stable vs. still evolving.

@juliekye

juliekye commented Sep 9, 2026

Copy link
Copy Markdown

Machine-readable cancellation policy fields

The refundability tri-state plus free-text description is a good start, but agents will be asked "what happens if I cancel Tuesday?" and need to answer deterministically. As a hotel company operating cancellation and guarantee policies across thousands of properties, we'd suggest structured fields alongside the enum: a cancellation_deadline (timestamp), a penalty expression (amount or basis, e.g. first night + tax), a modifiable indicator (modification and cancellation are distinct policies at most hotels), and a guarantee-vs-charge distinction — many hotel bookings take a card only as a guarantee, with payment collected at the property, which also interacts with the payment-required discussion elsewhere in this PR.

Leaving penalties in free text + URL pushes agents back toward parsing policy prose, which reintroduces exactly the ambiguity this extension exists to remove.

@juliekye

juliekye commented Sep 9, 2026

Copy link
Copy Markdown

Partial operation support / conformance floor

Is a business conformant if it supports create / get / complete / cancel but not update_booking_session (handling changes as cancel-and-rebook), and can that subset be declared in the /.well-known/ucp manifest? If update is mandatory, please define minimum update semantics — full resource replacement vs. partial update, and which parts of a session are mutable. Lodging systems commonly stage rollouts operation-by-operation, so knowing the conformance floor determines launch timelines.

@juliekye

juliekye commented Sep 9, 2026

Copy link
Copy Markdown

Merchant-defined extensions / custom fields

What is the recommended pattern for merchant-specific data on booking sessions — loyalty identifiers, special requests, accessibility needs, marketing preferences? Checkout-based implementations commonly lean on free-form metadata objects today. If the lodging capability expects namespaced extensions instead, explicit guidance (or a reserved vendor-namespace convention) would keep partner data from forking the core schemas differently in every implementation.

@juliekye

juliekye commented Sep 9, 2026

Copy link
Copy Markdown

Loyalty membership and member-only rates

How should rate_plan represent member-only or otherwise eligibility-gated rates, and how does a platform assert a traveler's loyalty membership so the business can honor member pricing and accrue stay credit? Today this only works where a platform happens to have account linking with the merchant; the spec itself has no slot for loyalty identity.

Suggested: an eligibility indicator on rate_plan (e.g., public / member / negotiated), a defined mechanism for membership assertion, and defined agent behavior on eligibility failure — requires_escalation looks like a natural fit for a sign-in or enrollment step.

@juliekye

juliekye commented Sep 9, 2026

Copy link
Copy Markdown

Session state semantics: requires_escalation and complete_in_progress

Please define normative triggers and expected agent behavior for requires_escalation and complete_in_progress: what conditions move a session into each state, which party is responsible for moving it out, and timeout/expiry expectations. Guidance on mapping business-side reservation states (e.g., pending/held vs. committed) onto session states would also reduce divergence across implementations.

@yairsabag

Copy link
Copy Markdown

Thanks, @juliekye. On your machine-readable cancellation policy point, the deterministic "what happens if I cancel Tuesday?" case is exactly the gap explored in draft #808, following the discussion here.

It proposes an optional anchor-relative schedule alongside refundability and description, with executable vectors for cutoff boundaries and fallback behavior. Its scope is selecting the applicable cancellation terms for pre-purchase disclosure, not executing cancellations or refunds or inferring monetary bases. Compound penalty expressions such as first night + tax, modification rules, and guarantee-versus-charge semantics would need separate treatment.

Would your team be open to sharing one synthetic policy example with the expected terms just before and exactly at a cutoff? I can turn it into a test vector against the draft for your team to check, including any gaps in the representation. No confidential policy or customer data would be needed.

@juliekye

juliekye commented Sep 9, 2026

Copy link
Copy Markdown

@yairsabag Happy to — here's a synthetic example modeled on a common lodging pattern (no real policy data).

Synthetic policy (prose): "Free cancellation until 6:00 PM property-local time two days before check-in. Cancellations at or after that time are charged one night's room rate (excluding taxes and fees)."

Concrete reservation for the vector:

  • Property timezone: America/Phoenix (UTC-7, no DST)
  • Check-in (anchor): 2026-10-16 15:00 local = 2026-10-16T22:00:00Z = 1792188000
  • Cutoff: 2026-10-14 18:00 local = 2026-10-15T01:00:00Z = 1792026000 (anchor − 162000 s)
  • Nightly room rate: 15000 minor units (USD $150.00), 2-night stay

Expected selected terms:

Evaluation instant Expected terms
1792025999 (cutoff − 1 s; 17:59:59 property-local) Fully refundable — no penalty
1792026000 (exactly at cutoff; 18:00:00 property-local) Penalty applies — one night's room rate, 15000 USD minor units

That is, our expectation reads "until 6:00 PM" as strictly-before: free interval [booking, cutoff), penalty [cutoff, ∞). If the draft's half-open boundary lands the other way, that's exactly the divergence worth surfacing in the vector.

Two things this example intentionally probes, which may be representation gaps:

  1. Local wall-clock policy templates vs. absolute offsets. The policy is defined in property-local wall-clock terms ("6 PM two days before arrival"), so the anchor-relative offset is reservation-specific. We deliberately chose a non-DST timezone; the same policy at a DST-observing property, with a booking whose cutoff and check-in straddle a transition, shifts the offset by an hour — worth confirming the offsets/DST vectors cover the policy-template case, not only fixed offsets.
  2. Penalty basis. "One night's room rate excluding taxes and fees" — we'd like to confirm the shared-measure outcome can carry that basis explicitly, since (as you note) compound first-night+tax expressions are out of scope for feat: add deterministic schedules to lodging cancellation policies #808.

Glad to check the resulting vector against additional variants (multi-tier schedules, non-refundable-from-booking) if useful.

@yairsabag

Copy link
Copy Markdown

Thanks, @juliekye, I've added your synthetic example, with source attribution, to #808 in a30b613.

Boundary: your expectation matches the draft. The Phoenix schedule uses PT45H: 1792025999 selects the free tier; 1792026000 selects the penalty terms. Both are now executable cases.

Local-clock templates: you're right that the earlier offset/DST cases did not test template resolution. New, separate test-only checks use IANA timezone data to derive the chosen local cutoffs: Phoenix is 45 hours; the New York spring/fall variants I added are 44/46 hours. These are unambiguous local-time examples, not a general template compiler or a policy for repeated/nonexistent local times. The wire schedule remains reservation-specific resolved instants and elapsed offsets.

Penalty basis: not currently. The shared night measure and display_text do not machine-encode which nightly rate applies or the exclusion of taxes/fees. I kept a symbolic one-night case and a separate fixed_fee case carrying the Business-resolved 15000 minor units in root Booking currency USD. That verifies the concrete selected amount, not a portable pricing-basis formula; the explicit basis remains an open representation gap.

The suite passes 52 selection/fallback cases, 26 schema expectations, 3 local-policy derivations, and 8 complete outcome assertions. Could you check that the Phoenix cases preserve your intended semantics, and whether a Business-resolved concrete amount is useful for your integration alongside the remaining need for an explicit basis?

@mecolmg mecolmg left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor description suggestions, but otherwise LGTM 👍

Comment thread source/schemas/lodging/types/capacity.json Outdated
},
"total": {
"type": "integer",
"description": "Total number of occupants. MUST be equal to adults + sum(children.total).",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"description": "Total number of occupants. MUST be equal to adults + sum(children.total).",
"description": "Maximum number of total occupants (adult or child). If adults and/or children are present, this MUST be greater than or equal to max(adults, children.items[].total).",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the first part of the sentence, but kept the invariant the same what we have right now (adults + sum(children.total)) as I feel that's a tighter contract - you either specify the breakdown into adult and children OR you specify a bare-bone total.

Specifying partial combinations doesn't make much sense to me for 2 reasons:

  • Platforms may need to make inferences based on the difference for any missing category (e.g., having adult & total will mean that platforms need to infer children based on the difference)
  • What does it mean when we have total > adults + sum(children.total)? This would be allowed by the new wording, but what does it mean in practice, what is this difference?

Comment thread source/schemas/lodging/types/capacity.json Outdated
Comment thread source/schemas/lodging/types/capacity.json
Comment thread source/schemas/lodging/types/capacity.json Outdated
Comment thread source/schemas/lodging/types/booking_confirmation.json
Comment thread source/schemas/lodging/types/room_rate.json Outdated
Comment thread source/schemas/lodging/types/occupancy.json Outdated
Comment thread source/schemas/lodging/types/occupancy.json Outdated
"type": "object",
"description": "Request metadata.",
"required": [
"ucp_agent",

@niranjanmanjunath niranjanmanjunath Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The specification (mcp.md:87-90), all documentation examples, and the reference implementation in Retail Shopping (mcp.openrpc.json:52-68) all use kebab-case ('ucp-agent').
Should we maintain consistency with them and use 'ucp-agent' ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same observation for 'idempotency_key' as well

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

@niranjanmanjunath niranjanmanjunath Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we port the "Accepted completion" operation contract from Checkout into this section to clarify what operations are permitted while in complete_in_progress?

Retail Checkout already addresses this in checkout/index.md#accepted-completion. How about expanding line 366 with a similar operation table?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same flag.

complete_in_progress is in the enum and the lifecycle diagram, but the contract that gives it meaning isn't here. Checkout's Accepted completion defines what each operation does while Complete is in flight — Get MAY poll with bounded backoff and MUST stop at expires_at; Update MUST NOT start and the Business returns the unchanged session with a recoverable error; Complete MUST NOT start anew, with a narrow lost-response retry using the same idempotency key and a fresh key for any genuinely new Complete; Cancel only after fallback and continue_url handoff are exhausted, and the session is not canceled until the Business says status: canceled. None of that is in this doc, and hotel completions — PMS and channel-manager round trips — are exactly the slow, asynchronous case it exists for. Without it, a Platform that times out on Complete has no spec-defined safe recovery for a booking that may or may not have been confirmed.

Given the duplication point above, I'd rather this doc reference checkout's section with a one-paragraph lodging note than port fifty lines — but one or the other has to happen before complete_in_progress means anything here.

@juliekye

juliekye commented Sep 9, 2026

Copy link
Copy Markdown

@yairsabag Confirmed — the Phoenix cases match our intent exactly: 2026-10-15T00:59:59Z selects the free tier and 2026-10-15T01:00:00Z selects the penalty, with PT45H lining up with the 6:00 PM local cutoff. The New York 44/46-hour cases also resolve the same local rule correctly across the spring/fall transitions — thanks for adding those.

On the Business-resolved amount: yes, that works well for us. Our systems already calculate the exact charge for a specific reservation at booking time, so filling in a resolved fixed_fee in booking currency is natural on our side, and it gives an agent the right number to show a guest. We'd keep the symbolic night measure alongside it for explanation. Two notes on what's still missing:

  1. With only a resolved amount, an agent can show the fee but can't explain or double-check it — "one night's room rate, excluding taxes and fees" still lives only in display_text. So an explicit basis remains worth pursuing, even if outside feat: add deterministic schedules to lodging cancellation policies #808's scope.
  2. The resolved amount is a snapshot: fine for the life of a session, but worth a spec note that it reflects the reservation's terms at the time it was calculated, not a formula that can be recomputed later.

Thanks again for turning this around so quickly.

@yairsabag

Copy link
Copy Markdown

Thanks, @juliekye, your confirmation of both the boundary behavior and the Business-resolved amount is very helpful.

I've added a short snapshot clarification in #808: the amount reflects the reservation terms used in its calculation, not a reusable formula. Changes to those terms may require an updated Business-provided amount even within the same booking session.

One representation clarification: when using fixed_fee, the one-night explanation, including the exclusion of taxes and fees, stays in the policy's description. The current draft does not combine penalty.measure with a fixed_fee outcome; the structured measure belongs to the alternative unit_deduction outcome. An explicit machine-readable basis, or a combined symbolic-and-resolved representation, remains a separate design question. This follow-up changes documentation only.

@igrigorik igrigorik left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this is great work!

Left a number of questions and suggestions. Some are simple nits and easy to fix divergences, but the big ones I want to flag and align on are...

  1. Schema is "room" centric, which under-models the domain, and it would be a breaking change to fix this + have ripple consequences through prose and schemas. As a request to the Lodging TC, please revisit this and explore how we can make this generic+polymorphic. This is not a novel pattern for UCP, we just need to carefully apply it here.
  2. We (re)invented due-now vs due-later in the payment schemas, with broken invariants over sum+verify that we need to restore. PTAL below.
  3. Remove -sessions to match shape of shopping operations.

Wrestled with the payment for a while, I think we took a small (wrong) detour...

1. The new totals[] types fail the shared verification rule. checkout/index.md § Verification — the normative home of totals.json — lets a Platform verify sum(non-total entries) == total, and on mismatch says it MUST NOT autonomously complete. due_now and due_at_property are sums of their sibling entries in the same flat array, so every example that uses them double-counts:

Pricing Scope example Σ non-total total
Pattern 1 (property-collected) 152 800 76 400 2× — Platform must not complete
Pattern 2 (upfront + savings) 260 000 130 000
Pattern 3 (deposit + balance) 270 000 135 000
rest.md / mcp.md examples (no new types) = = pass

Because totals[].type is an open vocabulary, this can't be fixed by exempting named types in a generic verifier -- the rule only works if every entry is an addend. Two smaller things ride along: root subtotal becomes the deposit (40 000) while room_rates[0].totals.subtotal is the full stay (120 000), so one shared type now carries two meanings of subtotal; and the deferred MUST keys on a value that terms.md says "carr[ies] no protocol meaning beyond 'not immediate'".

2. Payment Terms already models this. terms.md opens with "A lodging Business can offer one term that charges the full stay at booking and another that charges the first night now and the balance at check-in", and its worked example is the same pt_pay_now / pt_deposit_balance scenario this PR uses. Three of its rules decide the placement question:

  • totals is price; schedules are timing. "Where the selected term changes what the purchase costs — a discount for paying today, a finance charge — that difference MUST appear as its own entry in checkout.totals." Only price deltas enter totals; the amount due today is the sum of the selected term's immediate schedules.
  • The due-now / due-later split is a Platform-derived view, not wire data: "type and due_at let a Platform that wants to do more — split the checkout into due-now and due-later, sort schedules, drive a calendar reminder — without ever being required to."
  • On the "terms is optional, so core totals must be unambiguous alone" point from the earlier thread: terms handles optionality by making silence mean due-in-full — "Where payment is due in full at completion, a Business may omit terms… and the Checkout total is the amount due." A Business with deferred or property-collected timing therefore advertises terms (it's common/, with a deliberately trivial presentation floor), the same way a Business with shipping choices advertises fulfillment. A single term is explicitly "disclosing rather than asking."

The PR's payment.terms[] block is already correct. The proposal is just that totals[] shrinks back to price, and the split is read off the schedules.

3. What Patterns 3 and 1 look like under that. Deposit & balance (same numbers as today):

"totals": [
  { "type": "subtotal", "display_text": "Room rate (3 nights, 1 room)", "amount": 120000,
    "lines": [ { "display_text": "Jul 15", "amount": 40000 },
               { "display_text": "Jul 16", "amount": 40000 },
               { "display_text": "Jul 17", "amount": 40000 } ] },
  { "type": "fee",   "display_text": "Service fee",             "amount": 3000 },
  { "type": "tax",   "display_text": "State lodging tax (10%)", "amount": 12000 },
  { "type": "total", "display_text": "Total stay cost",         "amount": 135000 }
],
"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 its service fee and tax." },
      "schedules": [
        { "id": "sched_first_night", "type": "immediate",
          "description": { "plain": "Due today when you book: first night, service fee, and tax on the deposit." },
          "amount": 47000 },
        { "id": "sched_balance", "type": "deferred",
          "description": { "plain": "Due at check-in on July 15, 2026 at 3:00 PM PDT: remaining 2 nights and tax." },
          "due_at": "2026-07-15T15:00:00-07:00", "amount": 88000 } ] }
  ]
}

Σ non-total = 135 000 = total; selected term schedules 47 000 + 88 000 = 135 000; a Platform that wants "Due today $470 / Due at check-in $880" computes it from type. Property-collected charges (Pattern 1) are ordinary fee / tax entries — they are part of the price — and their timing is one disclosing term:

"totals": [
  { "type": "subtotal", "display_text": "Room rate (3 nights, 1 room, incl. 10% consumption tax)", "amount": 64000 },
  { "type": "fee",      "display_text": "Service fee (10%)",                                        "amount": 6400 },
  { "type": "fee",      "display_text": "Resort & facility fee — paid at the hotel (¥2,000/night)",  "amount": 6000 },
  { "type": "tax",      "display_text": "Tokyo accommodation tax — paid at the hotel (¥200/guest/night)", "amount": 1200 },
  { "type": "total",    "display_text": "Total stay cost",                                          "amount": 77600 }
],
"payment": {
  "selected_term_id": "pt_standard",
  "terms": [
    { "id": "pt_standard", "title": "Room charged now, hotel fees at check-in",
      "description": { "plain": "Your room and service fee are charged today. The resort fee and Tokyo accommodation tax are collected by the hotel when you check in." },
      "schedules": [
        { "id": "sched_room",     "type": "immediate",
          "description": { "plain": "Due today when you book: room rate and service fee." }, "amount": 70400 },
        { "id": "sched_property", "type": "at_property",
          "description": { "plain": "Collected by the hotel at check-in: resort & facility fee and Tokyo accommodation tax." },
          "amount": 7200 } ] }
  ]
}

Both validate against payment_terms.json#/$defs/dev.ucp.lodging.booking and pass the checkout assert. The local-tax disclosure then targets the schedule (path: "$.payment.terms[0].schedules[1]"), which is where terms.md puts timing disclosures, and doesn't shift when totals are recomputed.

4. The one real gap, and where it belongs. Pattern 1's second schedule is collected by the property, not charged to the instrument the Platform supplies. terms.md currently says the funding instrument "MUST be capable of every one of [the term's schedules]", which a pay-at-desk schedule doesn't fit. That's a one-sentence clarification to terms.md (a schedule whose description states it is settled directly with the Business at a future event isn't funded through this checkout).

Concretely: drop due_now, postpaid_subtotal, postpaid_fee, postpaid_tax, due_at_property and the accounting-invariants table; keep total all-in and subtotal as the full-stay room charge (so Σ room_rates[].totals.subtotal = root subtotal); replace "Immediate vs. Deferred Payment Breakdown" with a short "Payment timing" section that points at terms.md and shows Pattern 1 as a single term; re-run the sum check over the examples. Happy to push a branch with exactly that diff if it's easier to review as code than as a comment.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"locks" suggests reservation, but I assume reservation does not happen until payment? Or if it is, how does this reservation behave? TTL expiry? Release mechanism? What happens when agent rolls through 15 different requests by mistake.. does that lock up inventory for next X minutes?

* **`canceled`**: Booking session is invalid or expired. Platform should
start a new booking session if needed.

### Actions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meta: we're duplicating actinos, error handling, ..., across verticals, which is quick and easy path for divergence and subtle bugs. We should explore how we can consolidate these surfaces. Non-blocking.

room capacity bounds, calculates totals (`totals[]`), and attaches
authoritative cancellation terms (`policies[]`).

### Pricing Scope

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're citing US and EU, but that excludes many other regions, I'd suggest we generalize to "applicable consumer price-display law".


| Tool | Operation | Description |
| :------------------------- | :------------------------------------------------------------------ | :------------------------- |
| `create_booking_session` | [Create Booking Session](index.md#create-booking-session) | Create a booking session. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, why do we need _session suffix everywhere? We explicitly avoided it in shopping: get_booking, create_booking, etc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an explicit differentiation as the order equivalent in lodging industry is also a "booking", so for operations like cancel_booking this may be confused with the post-payment operation to actually cancel the entire booking/reservation.

Adding the _session suffix makes it clear that all operations pertain only to the active booking session and has no side effects - this is also consistent with the REST checkout modelling in shopping - https://ucp.dev/latest/specification/shopping/checkout/rest/#operations.

Comment on lines +32 to +61
"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."
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The meta schema here diverges from every other UCP MCP service on the wire. shopping/mcp.openrpc.json has used ucp-agent / idempotency-key (header-style, "Maps to HTTP UCP-Agent header") since #154 and never changed; overview/index.md, cart/mcp.md, order/mcp.md all match. This file requires ucp_agent / idempotency_key — so a Platform's shared MCP envelope code fails validation against a lodging server and vice versa, which is the one thing a common/ envelope exists to prevent.

Two things make it worth fixing before merge rather than after:

  • Every meta example in this PR's own mcp.md (lines 87, 157, 395, 640) uses the hyphenated keys, so the doc's examples are invalid against this schema. CI can't see it — the ucp:example macro extracts $.params.arguments.booking and never validates meta.
  • ucp-schema lint already flags it: lodging/mcp.openrpc.json W007 "ucp_agent is not a UCP annotation and has no effect; the ucp_ prefix is reserved." Shopping's mcp.openrpc.json produces no such warning.

Separately, required here lists idempotency_key for every op, including get_booking_session, whereas shopping requires it at the meta level only for ucp-agent and narrows complete/cancel with an allOf (see the two threads on those methods). The suggestion is shopping's block verbatim:

Suggested change
"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."
}
}
}
"meta": {
"type": "object",
"description": "Request metadata.",
"required": [
"ucp-agent"
],
"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."
}
}
}

This is a concrete instance of the duplication point I raised on index.md — the envelope (meta, header set, idempotency prose) wants to live once in services/common/ and be $ref'd by each vertical, or the next vertical forks it again.

Comment on lines +16 to +19
"role": {
"type": "string",
"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."
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The open-vocabulary sentence here is copied from response-side fields and points the wrong way for a request field. guest_assignments[] is Platform-written (create/update), so the Platform never meets a role it doesn't know — "platforms MUST tolerate unknown values" has no one to bind. And "Businesses MAY define additional values" has no wire: there's no discovery field, capability config, or messages[] pattern through which a Business could tell a Platform which roles exist (where the corpus wants Business-defined choices a Platform picks from, it publishes them with ids — fulfillment options[], payment terms[]). What's missing is the receiver's clause: what a Business does with role: "vip_host".

Since primary_guest is the only role with protocol semantics — the guest the folio and the lead-identity floor attach to — the safe degradation is the payment_schedule.type shape ("immediate is the only value with defined meaning; any other value means…"):

Suggested change
"role": {
"type": "string",
"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."
}
"role": {
"type": "string",
"description": "Role of the guest in this room. `primary_guest` is the only value with defined meaning: the guest the reservation and folio are attached to. `additional_guest` is the well-known value for every other occupant. Platforms MAY send additional values; a Business MUST treat any value it does not recognize as `additional_guest`."
}

},
"travel_purpose": {
"type": "string",
"description": "Purpose of the trip or reservation. Well-known values: `business`, `leisure`. Businesses MAY implement and support additional values.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same direction issue as guest_assignments[].role: this is a Platform-written hint, so the clause that matters is what the Business does with a value it doesn't support (e.g. if business gates a corporate rate, what does bleisure do?). "Businesses MAY implement and support additional values" is fine as far as it goes; it just never says the receiver's behavior.

Suggested change
"description": "Purpose of the trip or reservation. Well-known values: `business`, `leisure`. Businesses MAY implement and support additional values.",
"description": "Purpose of the trip or reservation, supplied by the Platform as a hint. Well-known values: `business`, `leisure`. Platforms MAY send additional values; a Business MUST ignore values it does not recognize and treat the purpose as unspecified.",

Comment on lines +37 to +45
"image_urls": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
},
"description": "List of image URIs for the room type.",
"ucp_request": "omit"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bare URIs drop the printable string. The corpus already has a media type — common/types/media.json (type, url, alt_text, width, height), used by product.media[] and variant.media[] with "first item is the featured media" — and for a non-text asset alt_text is what a text-only agent or a screen reader presents. image_urls: string[] gives them nothing.

It also under-fits the vertical. #690 (open) extends Media into image / video / model_3d variants with sources[], a preview still, and name distinct from alt_text. Hotels are the media-heavy case that shape was built for — galleries, video walkthroughs, and 360°/3D room tours are standard on every booking surface — and none of it is expressible as a list of image URIs. Adopting media[] here means lodging inherits that for free when it lands; shipping image_urls means a breaking rename later.

Suggested change
"image_urls": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
},
"description": "List of image URIs for the room type.",
"ucp_request": "omit"
}
"media": {
"type": "array",
"items": {
"$ref": "../../common/types/media.json"
},
"description": "Room type media (images, videos, 3D tours). First item is the featured media for presentation.",
"ucp_request": "omit"
}

(Same change on accommodation.json. No example payload uses image_urls today, so this is a schema-only rename.)

Comment on lines +14 to +22
"image_urls": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
},
"description": "List of accommodation image URIs.",
"ucp_request": "omit"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as room_type.image_urls — use the shared media type so the asset carries its alt_text (and, via #690, video and 3D tours). Property-level media is where a virtual tour most often lives.

Suggested change
"image_urls": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
},
"description": "List of accommodation image URIs.",
"ucp_request": "omit"
}
"media": {
"type": "array",
"items": {
"$ref": "../../common/types/media.json"
},
"description": "Accommodation media (images, videos, 3D tours). First item is the featured media for presentation.",
"ucp_request": "omit"
}

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same flag.

complete_in_progress is in the enum and the lifecycle diagram, but the contract that gives it meaning isn't here. Checkout's Accepted completion defines what each operation does while Complete is in flight — Get MAY poll with bounded backoff and MUST stop at expires_at; Update MUST NOT start and the Business returns the unchanged session with a recoverable error; Complete MUST NOT start anew, with a narrow lost-response retry using the same idempotency key and a fresh key for any genuinely new Complete; Cancel only after fallback and continue_url handoff are exhausted, and the session is not canceled until the Business says status: canceled. None of that is in this doc, and hotel completions — PMS and channel-manager round trips — are exactly the slow, asynchronous case it exists for. Without it, a Platform that times out on Complete has no spec-defined safe recovery for a booking that may or may not have been confirmed.

Given the duplication point above, I'd rather this doc reference checkout's section with a one-paragraph lodging note than port fifty lines — but one or the other has to happen before complete_in_progress means anything here.

"ucp_request": "omit"
},
"actions": {
"$ref": "../common/types/actions.json",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @jingyli - The actions schema allows extension-defined Action types using reverse-domain keys. Should UCP define a set of standardized/well-known Action types for common flows (e.g., 3DS authentication) so that businesses and platforms use consistent semantics, while still allowing custom extension-defined Actions?

"address": {
"$ref": "../../common/types/postal_address.json",
"description": "Physical or registered address of the booker or company for legal contracting, regulatory compliance, and invoicing."
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @jingyli - For a travel agency making a booking through the UCP platform, how should the platform pass agency-specific information, such as agency name, IATA number, agency/agent ID, and contact details, to the business? The current booker node does not provide dedicated fields for capturing the travel agency or agent facilitating the booking.

Comment on lines +14 to +41
"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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jingyli this was my intention by the suggestion of an inequality in this comment.

The callout on maintaining naming consistency with occupancy is also a good callout. The proposed structure here I think makes sense and simplifies enforcement by Platforms and Businesses.

},
"role": {
"type": "string",
"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."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conversely, role: "additional" does not have the same level of intuitive implication. Perhaps we should suggest secondary instead of additional?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:lodging Issues and pull requests related to the Lodging vertical area:payments Issues and pull requests related to the Payments vertical status:under-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.