# Time slots (`time_slots`)

Let the person pick one or several time slots across one or more days (tables, appointments, meetings). The page records the choice; it does not reserve the slot.

**When to use it:** The person picks one or more times from open slots grouped by day: a table, an appointment, a meeting. The page only records the choice: reserve or schedule it yourself afterwards, and do not tell the person it is confirmed until you have.

**When not to use it:** Do not use it to find any free time on the person's calendar (it shows only the slots you supply), or for an open-ended date (use `date_range`).

**Gather first:**

- The real open slots from the venue, calendar or service, for the next few days.
- Whether the person should pick one slot or several (`multiple`).

**Writing good content:**

- Group slots by day, earliest first. Give each slot a `start` with the venue's UTC offset and set `timeZone` so the page shows local time.
- Mark slots that just filled up with `available: false` rather than removing them, so the grid stays readable.
- Use `label` for a short extra like "Patio", and `price` only when slots cost different amounts.
- The answer is the chosen slot ids. Request the booking 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 |
| --- | --- |
| `timeZone` | *string.* Optional IANA time zone the times are shown in, e.g. "America/Los_Angeles". It is displayed on the page. Include when known; omit otherwise and each time is shown at the UTC offset written in it. |
| `days` | *object[], required.* Days with their slots, in display order (1-14 days). Limits: 1-14 items. |
| `days[].date` | *string, required.* The day these slots fall on, "YYYY-MM-DD", in timeZone (or in the slot start's own UTC offset when timeZone is omitted). Days are shown in the order given. |
| `days[].slots` | *object[], required.* Slots on this day, earliest first (1-48). Limits: 1-48 items. |
| `days[].slots[].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}$`. |
| `days[].slots[].start` | *ISO 8601 timestamp, required.* Start time as ISO 8601 with UTC offset, e.g. "2026-11-03T19:30:00-08:00". |
| `days[].slots[].end` | *ISO 8601 timestamp.* Optional end time as ISO 8601 with UTC offset, e.g. "2026-11-03T19:30:00-08:00". |
| `days[].slots[].label` | *string.* Short extra text on the chip, e.g. "Bar seating" or "Dr. Rivera". Limits: 1-60 characters. |
| `days[].slots[].price` | *object.* Optional price for this slot. Include only when slots cost different amounts. |
| `days[].slots[].price.amount` | *number, required.* Amount in major units, e.g. 249.99 for $249.99. Limits: 0-10000000. |
| `days[].slots[].price.currency` | *string, required.* ISO 4217 currency code, uppercase, e.g. "USD", "EUR". |
| `days[].slots[].available` | *boolean.* false shows the slot as disabled (taken). Omit for an open slot. Keep taken slots so the person sees them. |
| `multiple` | *boolean.* true lets the person pick several slots. Omit or false for exactly one. |
| `minSelections` | *integer.* Only when multiple is true: the fewest slots required. Defaults to 1. Limits: at least 1. |
| `maxSelections` | *integer.* Only when multiple is true: the most slots allowed. Defaults to all available. Limits: at least 1. |

**Why these are required:** A slot needs an id and a time, grouped by day.

**When optional fields are left out:** Times are shown at each timestamp's UTC offset and the "Times shown in" line is hidden; single choice; a slot is open unless `available` is `false`.

## Minimal example

Only the required fields:

```json
{
  "version": 1,
  "title": "Pick a time for your bike tune-up",
  "page": {
    "type": "time_slots",
    "days": [
      {
        "date": "2026-11-10",
        "slots": [
          {
            "id": "t0900",
            "start": "2026-11-10T09:00:00-05:00"
          },
          {
            "id": "t1100",
            "start": "2026-11-10T11:00:00-05:00"
          },
          {
            "id": "t1400",
            "start": "2026-11-10T14:00:00-05:00"
          }
        ]
      },
      {
        "date": "2026-11-11",
        "slots": [
          {
            "id": "w1000",
            "start": "2026-11-11T10:00:00-05:00"
          },
          {
            "id": "w1530",
            "start": "2026-11-11T15:30:00-05:00"
          }
        ]
      }
    ]
  }
}
```

## Full example

```json
{
  "version": 1,
  "title": "Pick a time at Ember & Stone",
  "description": "Table for four. Pick the time that suits you.",
  "branding": {
    "name": "Juniper Concierge",
    "accentColor": "#0050d8"
  },
  "cover": {
    "url": "https://images.example.test/ember.jpg",
    "alt": "Wood-fired pizza coming out of the oven at Ember & Stone"
  },
  "page": {
    "type": "time_slots",
    "timeZone": "America/Los_Angeles",
    "multiple": false,
    "days": [
      {
        "date": "2026-11-06",
        "slots": [
          {
            "id": "f1730",
            "start": "2026-11-06T17:30:00-08:00",
            "available": true
          },
          {
            "id": "f1800",
            "start": "2026-11-06T18:00:00-08:00",
            "available": false
          },
          {
            "id": "f1830",
            "start": "2026-11-06T18:30:00-08:00",
            "available": true
          },
          {
            "id": "f1900",
            "start": "2026-11-06T19:00:00-08:00",
            "available": true
          },
          {
            "id": "f1930",
            "start": "2026-11-06T19:30:00-08:00",
            "available": false
          },
          {
            "id": "f2000",
            "start": "2026-11-06T20:00:00-08:00",
            "available": true
          }
        ]
      },
      {
        "date": "2026-11-07",
        "slots": [
          {
            "id": "s1730",
            "start": "2026-11-07T17:30:00-08:00",
            "available": true
          },
          {
            "id": "s1800",
            "start": "2026-11-07T18:00:00-08:00",
            "available": true
          },
          {
            "id": "s1900",
            "start": "2026-11-07T19:00:00-08:00",
            "available": false
          }
        ]
      }
    ]
  }
}
```

## Result

When the person submits, the result has these fields:

| Field | Description |
| --- | --- |
| `selectedSlotIds` | *string[], required.* ids of the chosen slots, in display order. Limits: 1-100 items. |

For the full example above, a result could be:

```json
{
  "selectedSlotIds": [
    "f1900"
  ]
}
```

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

```json
{
  "title": "Pick a time at Ember & Stone",
  "description": "Table for four. Pick the time that suits you.",
  "cover": {
    "url": "https://images.example.test/ember.jpg",
    "alt": "Wood-fired pizza coming out of the oven at Ember & Stone"
  },
  "page": {
    "type": "time_slots",
    "timeZone": "America/Los_Angeles",
    "multiple": false,
    "days": [
      {
        "date": "2026-11-06",
        "slots": [
          {
            "id": "f1730",
            "start": "2026-11-06T17:30:00-08:00",
            "available": true
          },
          {
            "id": "f1800",
            "start": "2026-11-06T18:00:00-08:00",
            "available": false
          },
          {
            "id": "f1830",
            "start": "2026-11-06T18:30:00-08:00",
            "available": true
          },
          {
            "id": "f1900",
            "start": "2026-11-06T19:00:00-08:00",
            "available": true
          },
          {
            "id": "f1930",
            "start": "2026-11-06T19:30:00-08:00",
            "available": false
          },
          {
            "id": "f2000",
            "start": "2026-11-06T20:00:00-08:00",
            "available": true
          }
        ]
      },
      {
        "date": "2026-11-07",
        "slots": [
          {
            "id": "s1730",
            "start": "2026-11-07T17:30:00-08:00",
            "available": true
          },
          {
            "id": "s1800",
            "start": "2026-11-07T18:00:00-08:00",
            "available": true
          },
          {
            "id": "s1900",
            "start": "2026-11-07T19:00:00-08:00",
            "available": false
          }
        ]
      }
    ]
  }
}
```

### 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":"Pick a time at Ember & Stone","description":"Table for four. Pick the time that suits you.","branding":{"name":"Juniper Concierge","accentColor":"#0050d8"},"cover":{"url":"https://images.example.test/ember.jpg","alt":"Wood-fired pizza coming out of the oven at Ember & Stone"},"page":{"type":"time_slots","timeZone":"America/Los_Angeles","multiple":false,"days":[{"date":"2026-11-06","slots":[{"id":"f1730","start":"2026-11-06T17:30:00-08:00","available":true},{"id":"f1800","start":"2026-11-06T18:00:00-08:00","available":false},{"id":"f1830","start":"2026-11-06T18:30:00-08:00","available":true},{"id":"f1900","start":"2026-11-06T19:00:00-08:00","available":true},{"id":"f1930","start":"2026-11-06T19:30:00-08:00","available":false},{"id":"f2000","start":"2026-11-06T20:00:00-08:00","available":true}]},{"date":"2026-11-07","slots":[{"id":"s1730","start":"2026-11-07T17:30:00-08:00","available":true},{"id":"s1800","start":"2026-11-07T18:00:00-08:00","available":true},{"id":"s1900","start":"2026-11-07T19:00:00-08:00","available":false}]}]}}}'
```

## 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 `days`:**

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

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

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