Restaurants (restaurant_picker)
Let the person choose one restaurant and, if it lists table times, one time. The page records the choice; it does not reserve a table. The result is { restaurantId, time? }.
When to use it: The person chooses a restaurant, optionally with a table time, from cards with photos, ratings and distance. The page only records the choice: reserve it yourself afterwards, and do not tell the person it is reserved until you have.
When not to use it: Do not use it for one restaurant (ask in chat) or to collect a general time preference with no restaurants (use time_slots).
Gather first:
- Party size, date and roughly when, and the area.
- Which tables are actually open, if you will offer
times: list only times you can request.
Writing good content:
- Offer 3 to 6 restaurants, best first, each with
cuisine,priceLeveland distance when you know them. - Add
timesonly from real availability, with the restaurant's own UTC offset, and setpartySizeso the person knows what the times are for. - Add
photoUrlonly with a real photo; leave it out otherwise. - The answer is a restaurant and maybe a time. Reserve it yourself afterwards, and say it is booked only once it is.
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 |
|---|---|
partySize | integer. Optional party size the listed times are for. Shown above the options. Limits: 1-20. |
options | object[], required. Restaurants 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. Restaurant name, e.g. "Lotus Thai Kitchen". Required. Limits: 1-80 characters. |
options[].cuisine | string. Optional cuisine or style, e.g. "Thai". Include when known. Limits: 1-40 characters. |
options[].priceLevel | integer. Optional price level from 1 ($) to 4 ($$$$). Include when known. Limits: 1-4. |
options[].rating | number. Optional average rating from 0 to 5, e.g. 4.6. Limits: 0-5. |
options[].reviewCount | integer. Optional number of reviews behind the rating. Needs rating. Limits: 0-10000000. |
options[].distance | object. Optional distance from the person or a reference point. |
options[].distance.value | number, required. Distance from the person, e.g. 0.4. Limits: at most 1000. |
options[].distance.unit | "mi" | "km", required. "mi" or "km". |
options[].address | string. Optional neighborhood or street address, e.g. "Mission District" or "412 Valencia St". Include when known. Limits: 1-120 characters. |
options[].photoUrl | https URL. Optional https URL of one photo. A blank box is shown if it fails to load. Omit if you have none. |
options[].timeZone | string. Optional IANA time zone of the restaurant; times are shown in this zone. Omit and each time is shown at the UTC offset written in it. |
options[].times | ISO 8601 timestamp[]. Optional table times you found for the party size, earliest first (1-12). When given, the person must pick one. Page Systems does not hold them. Omit if you have no availability data. Limits: 1-12 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 restaurant needs an id and a name.
When optional fields are left out: Those details are left out of the card. With times but no timeZone, times use the timestamp's UTC offset. Without times the result is { restaurantId }.
Minimal example
Only the required fields:
{
"version": 1,
"title": "Where should we have lunch?",
"page": {
"type": "restaurant_picker",
"options": [
{
"id": "saltgrass",
"name": "Saltgrass Diner"
},
{
"id": "pho-loft",
"name": "Pho Loft"
},
{
"id": "verde",
"name": "Casa Verde Taqueria"
}
]
}
}Full example
{
"version": 1,
"title": "Dinner on Friday",
"description": "Pick a place and a time, and I’ll take it from there.",
"branding": {
"name": "Juniper Concierge",
"accentColor": "#b3411f"
},
"page": {
"type": "restaurant_picker",
"partySize": 4,
"options": [
{
"id": "lotus",
"name": "Lotus Thai Kitchen",
"cuisine": "Thai",
"priceLevel": 2,
"rating": 4.6,
"reviewCount": 1284,
"distance": {
"value": 0.4,
"unit": "mi"
},
"address": "Mission District",
"photoUrl": "https://images.example.test/lotus.jpg",
"timeZone": "America/Los_Angeles",
"times": [
"2026-11-06T18:30:00-08:00",
"2026-11-06T19:00:00-08:00",
"2026-11-06T19:30:00-08:00",
"2026-11-06T20:15:00-08:00"
]
},
{
"id": "ember",
"name": "Ember & Stone Pizza",
"cuisine": "Pizza",
"priceLevel": 1,
"rating": 4.3,
"reviewCount": 612,
"distance": {
"value": 0.9,
"unit": "mi"
},
"address": "412 Valencia St",
"photoUrl": "https://images.example.test/ember.jpg",
"timeZone": "America/Los_Angeles",
"times": [
"2026-11-06T19:15:00-08:00",
"2026-11-06T20:45:00-08:00"
]
},
{
"id": "kumo",
"name": "Kumo Sushi Bar",
"cuisine": "Japanese",
"priceLevel": 4,
"rating": 4.8,
"distance": {
"value": 1.2,
"unit": "mi"
},
"address": "Hayes Valley",
"photoUrl": "https://images.example.test/kumo.jpg"
},
{
"id": "corner",
"name": "The Corner Table",
"cuisine": "American",
"priceLevel": 3,
"address": "Noe Valley"
}
]
}
}Result
When the person submits, the result has these fields:
| Field | Description |
|---|---|
restaurantId | string, required. id of the chosen restaurant. Limits: matches ^[A-Za-z0-9_-]{1,64}$. |
time | ISO 8601 timestamp. The chosen entry of that restaurant's times. Present only if it lists times. |
For the full example above, a result could be:
{
"restaurantId": "lotus",
"time": "2026-11-06T18:30:00-08:00"
}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 "restaurant_picker" (the same JSON as GET /v1/page-types/restaurant_picker), then create_page with this input (no version or branding; branding comes from your dashboard):
{
"title": "Dinner on Friday",
"description": "Pick a place and a time, and I’ll take it from there.",
"page": {
"type": "restaurant_picker",
"partySize": 4,
"options": [
{
"id": "lotus",
"name": "Lotus Thai Kitchen",
"cuisine": "Thai",
"priceLevel": 2,
"rating": 4.6,
"reviewCount": 1284,
"distance": {
"value": 0.4,
"unit": "mi"
},
"address": "Mission District",
"photoUrl": "https://images.example.test/lotus.jpg",
"timeZone": "America/Los_Angeles",
"times": [
"2026-11-06T18:30:00-08:00",
"2026-11-06T19:00:00-08:00",
"2026-11-06T19:30:00-08:00",
"2026-11-06T20:15:00-08:00"
]
},
{
"id": "ember",
"name": "Ember & Stone Pizza",
"cuisine": "Pizza",
"priceLevel": 1,
"rating": 4.3,
"reviewCount": 612,
"distance": {
"value": 0.9,
"unit": "mi"
},
"address": "412 Valencia St",
"photoUrl": "https://images.example.test/ember.jpg",
"timeZone": "America/Los_Angeles",
"times": [
"2026-11-06T19:15:00-08:00",
"2026-11-06T20:45:00-08:00"
]
},
{
"id": "kumo",
"name": "Kumo Sushi Bar",
"cuisine": "Japanese",
"priceLevel": 4,
"rating": 4.8,
"distance": {
"value": 1.2,
"unit": "mi"
},
"address": "Hayes Valley",
"photoUrl": "https://images.example.test/kumo.jpg"
},
{
"id": "corner",
"name": "The Corner Table",
"cuisine": "American",
"priceLevel": 3,
"address": "Noe Valley"
}
]
}
}REST
curl https://pagesystems.ai/v1/pages \
-H "Authorization: Bearer $PAGE_SYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"spec":{"version":1,"title":"Dinner on Friday","description":"Pick a place and a time, and I’ll take it from there.","branding":{"name":"Juniper Concierge","accentColor":"#b3411f"},"page":{"type":"restaurant_picker","partySize":4,"options":[{"id":"lotus","name":"Lotus Thai Kitchen","cuisine":"Thai","priceLevel":2,"rating":4.6,"reviewCount":1284,"distance":{"value":0.4,"unit":"mi"},"address":"Mission District","photoUrl":"https://images.example.test/lotus.jpg","timeZone":"America/Los_Angeles","times":["2026-11-06T18:30:00-08:00","2026-11-06T19:00:00-08:00","2026-11-06T19:30:00-08:00","2026-11-06T20:15:00-08:00"]},{"id":"ember","name":"Ember & Stone Pizza","cuisine":"Pizza","priceLevel":1,"rating":4.3,"reviewCount":612,"distance":{"value":0.9,"unit":"mi"},"address":"412 Valencia St","photoUrl":"https://images.example.test/ember.jpg","timeZone":"America/Los_Angeles","times":["2026-11-06T19:15:00-08:00","2026-11-06T20:45:00-08:00"]},{"id":"kumo","name":"Kumo Sushi Bar","cuisine":"Japanese","priceLevel":4,"rating":4.8,"distance":{"value":1.2,"unit":"mi"},"address":"Hayes Valley","photoUrl":"https://images.example.test/kumo.jpg"},{"id":"corner","name":"The Corner Table","cuisine":"American","priceLevel":3,"address":"Noe Valley"}]}}}'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