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:
nightlyPriceis per night with taxes when you know them; addtotalPricefor the whole stay so options compare fairly.- Set
checkInandcheckOutso the page shows the dates and number of nights. - Include
photos,stars,guestRatingandneighborhoodwhen you have them: they are what people compare. - State the cancellation policy only when your source says it; leave
refundableout 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. 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:
{
"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
{
"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:
{
"hotelId": "harbor"
}Send it
eve, AI SDK and MCP
With @pagesystems/eve, @pagesystems/ai-sdk or the MCP server, the model calls get_page_type_schema with "hotel_picker" (the same JSON as GET /v1/page-types/hotel_picker), then create_page with this input (no version or branding; branding comes from your dashboard):
{
"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
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:
✖ Invalid input: expected array, received undefined
→ at page.optionsMisspelling a field name (unknown keys are rejected):
✖ Unrecognized key: "colour"
→ at page