# Hotels (`hotel_picker`)

Let the person choose one hotel for a stay. The page records the choice; it does not book or hold a room. The result is { hotelId }.

**When to use it:** The person chooses a hotel for a stay. Each card shows the nightly and total price, rating and cancellation policy. The page only records the choice: book it yourself afterwards, and do not tell the person it is booked until you have.

**When not to use it:** Do not use it to choose dates (use `date_range` first) or to confirm a hotel that is already chosen.

**Gather first:**

- The check-in and check-out dates and the number of guests, so every price covers the same stay.
- Real availability and prices from your search for those dates.

**Writing good content:**

- `nightlyPrice` is per night with taxes when you know them; add `totalPrice` for the whole stay so options compare fairly.
- Set `checkIn` and `checkOut` so the page shows the dates and number of nights.
- Include `photos`, `stars`, `guestRating` and `neighborhood` when you have them: they are what people compare.
- State the cancellation policy only when your source says it; leave `refundable` out rather than guess.
- Use your provider's hotel or offer id as `id`, so you can act on exactly the one picked. The page only records the choice: book it yourself afterwards.

## Fields

Every page also has `version` (always `1`), `title`, and optionally `description`, `branding`, `cover` (a header photo) and `expiresAt`; see [Page fields](/docs/api#page-fields). These are the fields of `page` (the page type and its data):

| Field | Description |
| --- | --- |
| `checkIn` | *date (YYYY-MM-DD).* Optional check-in date, "YYYY-MM-DD". Same for every option. Include when known. |
| `checkOut` | *date (YYYY-MM-DD).* Optional check-out date, "YYYY-MM-DD", after checkIn. Include when known. |
| `options` | *object[], required.* Hotels in display order (1-30). Limits: 1-30 items. |
| `options[].id` | *string, required.* Stable identifier you choose. It is returned in the result, so keep it meaningful to you. Limits: matches `^[A-Za-z0-9_-]{1,64}$`. |
| `options[].name` | *string, required.* Hotel name, e.g. "Harbor Light Hotel". Required. Limits: 1-80 characters. |
| `options[].photos` | *https URL[].* Optional, up to 5 https photo URLs. The person swipes through them; ones that fail to load show a blank box. Omit if you have none. Limits: at most 5 items. |
| `options[].stars` | *integer.* Optional official star class, 1-5. Include when known. Limits: 1-5. |
| `options[].guestRating` | *number.* Optional guest rating on a 0-10 scale, e.g. 8.7. Limits: 0-10. |
| `options[].neighborhood` | *string.* Optional neighborhood or area, e.g. "Old Town". Include when known. Limits: 1-80 characters. |
| `options[].nightlyPrice` | *object, required.* Price per night, including taxes if you know them. Required. |
| `options[].nightlyPrice.amount` | *number, required.* Amount in major units, e.g. 249.99 for $249.99. Limits: 0-10000000. |
| `options[].nightlyPrice.currency` | *string, required.* ISO 4217 currency code, uppercase, e.g. "USD", "EUR". |
| `options[].totalPrice` | *object.* Optional price for the whole stay, in the same currency as nightlyPrice. Include when known. |
| `options[].totalPrice.amount` | *number, required.* Amount in major units, e.g. 249.99 for $249.99. Limits: 0-10000000. |
| `options[].totalPrice.currency` | *string, required.* ISO 4217 currency code, uppercase, e.g. "USD", "EUR". |
| `options[].amenities` | *string[].* Optional, up to 8 short amenities, e.g. ["Free Wi-Fi", "Pool"]. Limits: at most 8 items. |
| `options[].cancellationPolicy` | *string.* Optional cancellation policy in one sentence, e.g. "Free cancellation until Nov 1." Include when known. Limits: 1-200 characters. |
| `options[].refundable` | *boolean.* Optional: true if the rate can be cancelled for a refund, false if not. Omit when you do not know. |
| `options[].badges` | *string[].* Optional short highlights such as "Best value" (up to 3). Limits: at most 3 items. |
| `options[].recommended` | *boolean.* Optional. Mark at most one option you recommend, when you have a reason: true shows a "Recommended" label on it. Omit on the others. |
| `options[].recommendedReason` | *string.* Optional short reason shown with the label, e.g. "Nonstop, and only $30 more". Only with recommended: true. Limits: 1-80 characters. |

**Why these are required:** A stay is a booking decision, so each hotel shows a price.

**When optional fields are left out:** No dates line or "for N nights"; only the nightly price; no stars, rating, area, policy, or refundable claim; no "Recommended" label (same one-per-page rule as `flight_picker`).

## Minimal example

Only the required fields:

```json
{
  "version": 1,
  "title": "Pick a hotel near the conference",
  "page": {
    "type": "hotel_picker",
    "options": [
      {
        "id": "alder",
        "name": "Alder Court Hotel",
        "nightlyPrice": {
          "amount": 159,
          "currency": "USD"
        }
      },
      {
        "id": "tidewater",
        "name": "Tidewater Suites",
        "nightlyPrice": {
          "amount": 212,
          "currency": "USD"
        }
      },
      {
        "id": "pinecone",
        "name": "Pinecone Lodge",
        "nightlyPrice": {
          "amount": 98,
          "currency": "USD"
        }
      }
    ]
  }
}
```

## Full example

```json
{
  "version": 1,
  "title": "Hotels in Portland",
  "description": "Prices are for 1 room, 2 guests, taxes included.",
  "branding": {
    "name": "Juniper Travel",
    "accentColor": "#0b7a5c"
  },
  "page": {
    "type": "hotel_picker",
    "checkIn": "2026-11-03",
    "checkOut": "2026-11-06",
    "options": [
      {
        "id": "harbor",
        "name": "Harbor Light Hotel",
        "photos": [
          "https://images.example.test/harbor-1.jpg",
          "https://images.example.test/harbor-2.jpg",
          "https://images.example.test/harbor-3.jpg"
        ],
        "stars": 4,
        "guestRating": 8.7,
        "neighborhood": "Old Town",
        "nightlyPrice": {
          "amount": 189,
          "currency": "USD"
        },
        "totalPrice": {
          "amount": 567,
          "currency": "USD"
        },
        "amenities": [
          "Free Wi-Fi",
          "Pool",
          "Breakfast"
        ],
        "cancellationPolicy": "Free cancellation until Nov 1.",
        "refundable": true,
        "badges": [
          "Best value"
        ],
        "recommended": true,
        "recommendedReason": "Refundable, breakfast included, and well rated"
      },
      {
        "id": "cedar",
        "name": "Cedar Row Inn",
        "photos": [
          "https://images.example.test/cedar-1.jpg"
        ],
        "stars": 3,
        "guestRating": 7.9,
        "neighborhood": "University District",
        "nightlyPrice": {
          "amount": 124.5,
          "currency": "USD"
        },
        "totalPrice": {
          "amount": 373.5,
          "currency": "USD"
        },
        "amenities": [
          "Free parking",
          "Free Wi-Fi"
        ],
        "cancellationPolicy": "Non-refundable rate. No changes.",
        "refundable": false
      },
      {
        "id": "skyline",
        "name": "Skyline Grand",
        "photos": [
          "https://images.example.test/skyline-1.jpg",
          "https://broken.example.test/missing.jpg"
        ],
        "stars": 5,
        "neighborhood": "Financial District",
        "nightlyPrice": {
          "amount": 342,
          "currency": "USD"
        },
        "totalPrice": {
          "amount": 1026,
          "currency": "USD"
        },
        "cancellationPolicy": "Free cancellation until 48 hours before check-in.",
        "refundable": true,
        "badges": [
          "Top rated",
          "Spa"
        ]
      },
      {
        "id": "plain",
        "name": "Meadow Motel",
        "stars": 2,
        "guestRating": 6.8,
        "neighborhood": "Airport",
        "nightlyPrice": {
          "amount": 79,
          "currency": "USD"
        },
        "totalPrice": {
          "amount": 237,
          "currency": "USD"
        },
        "cancellationPolicy": "Free cancellation until check-in day.",
        "refundable": true
      }
    ]
  }
}
```

## Result

When the person submits, the result has these fields:

| Field | Description |
| --- | --- |
| `hotelId` | *string, required.* id of the chosen hotel. Limits: matches `^[A-Za-z0-9_-]{1,64}$`. |

For the full example above, a result could be:

```json
{
  "hotelId": "harbor"
}
```

## Send it

### eve, AI SDK and MCP

With [`@pagesystems/eve`](/docs#quickstart-eve), [`@pagesystems/ai-sdk`](/docs#quickstart-ai-sdk) or the [MCP server](/docs#quickstart-mcp), the model calls `get_page_type_schema` with `"hotel_picker"` (the same JSON as [`GET /v1/page-types/hotel_picker`](/docs/api#endpoints)), then `create_page` with this input (no `version` or `branding`; branding comes from your dashboard):

```json
{
  "title": "Hotels in Portland",
  "description": "Prices are for 1 room, 2 guests, taxes included.",
  "page": {
    "type": "hotel_picker",
    "checkIn": "2026-11-03",
    "checkOut": "2026-11-06",
    "options": [
      {
        "id": "harbor",
        "name": "Harbor Light Hotel",
        "photos": [
          "https://images.example.test/harbor-1.jpg",
          "https://images.example.test/harbor-2.jpg",
          "https://images.example.test/harbor-3.jpg"
        ],
        "stars": 4,
        "guestRating": 8.7,
        "neighborhood": "Old Town",
        "nightlyPrice": {
          "amount": 189,
          "currency": "USD"
        },
        "totalPrice": {
          "amount": 567,
          "currency": "USD"
        },
        "amenities": [
          "Free Wi-Fi",
          "Pool",
          "Breakfast"
        ],
        "cancellationPolicy": "Free cancellation until Nov 1.",
        "refundable": true,
        "badges": [
          "Best value"
        ],
        "recommended": true,
        "recommendedReason": "Refundable, breakfast included, and well rated"
      },
      {
        "id": "cedar",
        "name": "Cedar Row Inn",
        "photos": [
          "https://images.example.test/cedar-1.jpg"
        ],
        "stars": 3,
        "guestRating": 7.9,
        "neighborhood": "University District",
        "nightlyPrice": {
          "amount": 124.5,
          "currency": "USD"
        },
        "totalPrice": {
          "amount": 373.5,
          "currency": "USD"
        },
        "amenities": [
          "Free parking",
          "Free Wi-Fi"
        ],
        "cancellationPolicy": "Non-refundable rate. No changes.",
        "refundable": false
      },
      {
        "id": "skyline",
        "name": "Skyline Grand",
        "photos": [
          "https://images.example.test/skyline-1.jpg",
          "https://broken.example.test/missing.jpg"
        ],
        "stars": 5,
        "neighborhood": "Financial District",
        "nightlyPrice": {
          "amount": 342,
          "currency": "USD"
        },
        "totalPrice": {
          "amount": 1026,
          "currency": "USD"
        },
        "cancellationPolicy": "Free cancellation until 48 hours before check-in.",
        "refundable": true,
        "badges": [
          "Top rated",
          "Spa"
        ]
      },
      {
        "id": "plain",
        "name": "Meadow Motel",
        "stars": 2,
        "guestRating": 6.8,
        "neighborhood": "Airport",
        "nightlyPrice": {
          "amount": 79,
          "currency": "USD"
        },
        "totalPrice": {
          "amount": 237,
          "currency": "USD"
        },
        "cancellationPolicy": "Free cancellation until check-in day.",
        "refundable": true
      }
    ]
  }
}
```

### REST

```bash
curl https://pagesystems.ai/v1/pages \
  -H "Authorization: Bearer $PAGE_SYSTEMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"spec":{"version":1,"title":"Hotels in Portland","description":"Prices are for 1 room, 2 guests, taxes included.","branding":{"name":"Juniper Travel","accentColor":"#0b7a5c"},"page":{"type":"hotel_picker","checkIn":"2026-11-03","checkOut":"2026-11-06","options":[{"id":"harbor","name":"Harbor Light Hotel","photos":["https://images.example.test/harbor-1.jpg","https://images.example.test/harbor-2.jpg","https://images.example.test/harbor-3.jpg"],"stars":4,"guestRating":8.7,"neighborhood":"Old Town","nightlyPrice":{"amount":189,"currency":"USD"},"totalPrice":{"amount":567,"currency":"USD"},"amenities":["Free Wi-Fi","Pool","Breakfast"],"cancellationPolicy":"Free cancellation until Nov 1.","refundable":true,"badges":["Best value"],"recommended":true,"recommendedReason":"Refundable, breakfast included, and well rated"},{"id":"cedar","name":"Cedar Row Inn","photos":["https://images.example.test/cedar-1.jpg"],"stars":3,"guestRating":7.9,"neighborhood":"University District","nightlyPrice":{"amount":124.5,"currency":"USD"},"totalPrice":{"amount":373.5,"currency":"USD"},"amenities":["Free parking","Free Wi-Fi"],"cancellationPolicy":"Non-refundable rate. No changes.","refundable":false},{"id":"skyline","name":"Skyline Grand","photos":["https://images.example.test/skyline-1.jpg","https://broken.example.test/missing.jpg"],"stars":5,"neighborhood":"Financial District","nightlyPrice":{"amount":342,"currency":"USD"},"totalPrice":{"amount":1026,"currency":"USD"},"cancellationPolicy":"Free cancellation until 48 hours before check-in.","refundable":true,"badges":["Top rated","Spa"]},{"id":"plain","name":"Meadow Motel","stars":2,"guestRating":6.8,"neighborhood":"Airport","nightlyPrice":{"amount":79,"currency":"USD"},"totalPrice":{"amount":237,"currency":"USD"},"cancellationPolicy":"Free cancellation until check-in day.","refundable":true}]}}}'
```

## Validation errors

A spec that does not validate is refused with `400 invalid_spec`. The message names each problem and its path, so a model can fix the spec and try again. For example:

**Leaving out the required field `options`:**

```text
✖ Invalid input: expected array, received undefined
  → at page.options
```

**Misspelling a field name (unknown keys are rejected):**

```text
✖ Unrecognized key: "colour"
  → at page
```
