# 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`, `priceLevel` and distance when you know them.
- Add `times` only from real availability, with the restaurant's own UTC offset, and set `partySize` so the person knows what the times are for.
- Add `photoUrl` only 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](/docs/api#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:

```json
{
  "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

```json
{
  "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:

```json
{
  "restaurantId": "lotus",
  "time": "2026-11-06T18:30:00-08:00"
}
```

## 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 `"restaurant_picker"` (the same JSON as [`GET /v1/page-types/restaurant_picker`](/docs/api#endpoints)), then `create_page` with this input (no `version` or `branding`; branding comes from your dashboard):

```json
{
  "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

```bash
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`:**

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

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

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