# Flights (`flight_picker`)

Let the person choose one flight itinerary. The page records the choice; it does not book or hold a seat.

**When to use it:** The person chooses one flight from search results. Each option shows airline, times, stops and price. 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 before you have real search results, or to show one flight that is already chosen: say that in chat.

**Gather first:**

- Search for flights first, so you offer real itineraries with real prices.
- The person's dates, origin and destination, and how many travellers the prices are for.

**Writing good content:**

- Times are ISO 8601 with the UTC offset of the airport where the event happens, and `timeZone` is that airport's IANA zone: depart `"2026-11-03T08:15:00-08:00"` with `"America/Los_Angeles"`, arrive in Lisbon with `"Europe/Lisbon"`. Never convert times to the person's zone; the page shows local airport time.
- `airport` is the 3-letter IATA code; add `city` for clarity.
- `stops: []` for nonstop. For connections, list each stop with its layover in minutes.
- `price` is the total for the itinerary including taxes, with an ISO 4217 currency code.
- `badges` for at most two honest highlights ("Cheapest", "Fastest", "Nonstop").
- Use the airline's or search result's own identifier as `id` so you can act on exactly what was picked.

## 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 |
| --- | --- |
| `options` | *object[], required.* Flight options 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[].airline` | *string, required.* Marketing airline name, e.g. "Skyward Air". Required. Limits: 1-60 characters. |
| `options[].flightNumbers` | *string[].* Optional flight number for each segment, in order, e.g. ["SA 123", "SA 456"]. Include when known. Limits: 1-6 items. |
| `options[].depart` | *object, required.* Departure airport and time. airport and time are required. |
| `options[].depart.airport` | *string, required.* IATA airport code, e.g. "SFO". Limits: matches `^[A-Z]{3}$`. |
| `options[].depart.city` | *string.* Optional city name shown next to the code. Include when known; omit otherwise. Limits: 1-60 characters. |
| `options[].depart.time` | *ISO 8601 timestamp, required.* Scheduled time as ISO 8601 with UTC offset, e.g. "2026-11-03T08:15:00-08:00". |
| `options[].depart.timeZone` | *string.* Optional IANA time zone of the airport, e.g. "America/Los_Angeles". Include when known; omit otherwise and the time is shown at the UTC offset written in time. |
| `options[].arrive` | *object, required.* Final arrival airport and time. airport and time are required. |
| `options[].arrive.airport` | *string, required.* IATA airport code, e.g. "SFO". Limits: matches `^[A-Z]{3}$`. |
| `options[].arrive.city` | *string.* Optional city name shown next to the code. Include when known; omit otherwise. Limits: 1-60 characters. |
| `options[].arrive.time` | *ISO 8601 timestamp, required.* Scheduled time as ISO 8601 with UTC offset, e.g. "2026-11-03T08:15:00-08:00". |
| `options[].arrive.timeZone` | *string.* Optional IANA time zone of the airport, e.g. "America/Los_Angeles". Include when known; omit otherwise and the time is shown at the UTC offset written in time. |
| `options[].stops` | *object[].* Optional connection airports in order. Use [] for a nonstop flight; omit when you do not know the stops. Limits: at most 4 items. |
| `options[].stops[].airport` | *string, required.* IATA airport code, e.g. "SFO". Limits: matches `^[A-Z]{3}$`. |
| `options[].stops[].city` | *string.* Optional city name. Include when known; omit otherwise. Limits: 1-60 characters. |
| `options[].stops[].layoverMinutes` | *integer, required.* Time on the ground at this stop, in minutes. Limits: 1-2880. |
| `options[].price` | *object, required.* Total price for the itinerary, including taxes. Required. |
| `options[].price.amount` | *number, required.* Amount in major units, e.g. 249.99 for $249.99. Limits: 0-10000000. |
| `options[].price.currency` | *string, required.* ISO 4217 currency code, uppercase, e.g. "USD", "EUR". |
| `options[].cabin` | *"economy" \| "premium_economy" \| "business" \| "first".* Optional cabin class. Include when known; omit otherwise. |
| `options[].badges` | *string[].* Optional short highlights such as "Cheapest" or "Fastest" (up to 3). Limits: at most 3 items. |
| `options[].notes` | *string.* Optional short note, e.g. "Bags not included". Limits: 1-200 characters. |
| `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:** Who flies, from where to where, when, and for how much is the least a person needs to pick a flight.

**When optional fields are left out:** Times are shown at the UTC offset written in the timestamp; no stops line (unknown, so not "Nonstop"); no cabin, flight numbers, badges or note; no "Recommended" label. At most one option may set `recommended: true`, and `recommendedReason` is allowed only on that option.

## Minimal example

Only the required fields:

```json
{
  "version": 1,
  "title": "Pick a flight to Denver",
  "page": {
    "type": "flight_picker",
    "options": [
      {
        "id": "skw204",
        "airline": "Skyward Air",
        "depart": {
          "airport": "SEA",
          "time": "2026-11-12T07:40:00-08:00"
        },
        "arrive": {
          "airport": "DEN",
          "time": "2026-11-12T11:05:00-07:00"
        },
        "price": {
          "amount": 164,
          "currency": "USD"
        }
      },
      {
        "id": "blu88",
        "airline": "Bluefin Airlines",
        "depart": {
          "airport": "SEA",
          "time": "2026-11-12T13:15:00-08:00"
        },
        "arrive": {
          "airport": "DEN",
          "time": "2026-11-12T16:50:00-07:00"
        },
        "price": {
          "amount": 139,
          "currency": "USD"
        }
      }
    ]
  }
}
```

## Full example

```json
{
  "version": 1,
  "title": "Choose your flight to New York",
  "description": "Tuesday, November 3 · 1 adult · Prices include taxes.",
  "branding": {
    "name": "Juniper Travel",
    "accentColor": "#0b7a5c"
  },
  "page": {
    "type": "flight_picker",
    "options": [
      {
        "id": "zq101",
        "airline": "Skyward Air",
        "flightNumbers": [
          "ZQ 101"
        ],
        "depart": {
          "airport": "SFO",
          "city": "San Francisco",
          "timeZone": "America/Los_Angeles",
          "time": "2026-11-03T06:00:00-08:00"
        },
        "arrive": {
          "airport": "JFK",
          "city": "New York",
          "timeZone": "America/New_York",
          "time": "2026-11-03T14:25:00-05:00"
        },
        "stops": [],
        "price": {
          "amount": 219,
          "currency": "USD"
        },
        "cabin": "economy",
        "badges": [
          "Fastest"
        ],
        "recommended": true,
        "recommendedReason": "Nonstop, and only $30 more than the cheapest"
      },
      {
        "id": "bf220",
        "airline": "Bluefin Airlines",
        "flightNumbers": [
          "BF 220",
          "BF 1408"
        ],
        "depart": {
          "airport": "SFO",
          "city": "San Francisco",
          "timeZone": "America/Los_Angeles",
          "time": "2026-11-03T09:10:00-08:00"
        },
        "arrive": {
          "airport": "JFK",
          "city": "New York",
          "timeZone": "America/New_York",
          "time": "2026-11-03T19:55:00-05:00"
        },
        "stops": [
          {
            "airport": "DEN",
            "city": "Denver",
            "layoverMinutes": 65
          }
        ],
        "price": {
          "amount": 189,
          "currency": "USD"
        },
        "cabin": "economy",
        "badges": [
          "Cheapest"
        ],
        "notes": "Carry-on only; checked bags extra."
      },
      {
        "id": "ap9",
        "airline": "Aurora Pacific",
        "flightNumbers": [
          "AP 9"
        ],
        "depart": {
          "airport": "SFO",
          "city": "San Francisco",
          "timeZone": "America/Los_Angeles",
          "time": "2026-11-03T22:30:00-08:00"
        },
        "arrive": {
          "airport": "JFK",
          "city": "New York",
          "timeZone": "America/New_York",
          "time": "2026-11-04T07:05:00-05:00"
        },
        "stops": [],
        "price": {
          "amount": 412.5,
          "currency": "USD"
        },
        "cabin": "premium_economy",
        "notes": "Overnight flight with lie-flat seats."
      }
    ]
  }
}
```

## Result

When the person submits, the result has these fields:

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

For the full example above, a result could be:

```json
{
  "selectedOptionId": "zq101"
}
```

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

```json
{
  "title": "Choose your flight to New York",
  "description": "Tuesday, November 3 · 1 adult · Prices include taxes.",
  "page": {
    "type": "flight_picker",
    "options": [
      {
        "id": "zq101",
        "airline": "Skyward Air",
        "flightNumbers": [
          "ZQ 101"
        ],
        "depart": {
          "airport": "SFO",
          "city": "San Francisco",
          "timeZone": "America/Los_Angeles",
          "time": "2026-11-03T06:00:00-08:00"
        },
        "arrive": {
          "airport": "JFK",
          "city": "New York",
          "timeZone": "America/New_York",
          "time": "2026-11-03T14:25:00-05:00"
        },
        "stops": [],
        "price": {
          "amount": 219,
          "currency": "USD"
        },
        "cabin": "economy",
        "badges": [
          "Fastest"
        ],
        "recommended": true,
        "recommendedReason": "Nonstop, and only $30 more than the cheapest"
      },
      {
        "id": "bf220",
        "airline": "Bluefin Airlines",
        "flightNumbers": [
          "BF 220",
          "BF 1408"
        ],
        "depart": {
          "airport": "SFO",
          "city": "San Francisco",
          "timeZone": "America/Los_Angeles",
          "time": "2026-11-03T09:10:00-08:00"
        },
        "arrive": {
          "airport": "JFK",
          "city": "New York",
          "timeZone": "America/New_York",
          "time": "2026-11-03T19:55:00-05:00"
        },
        "stops": [
          {
            "airport": "DEN",
            "city": "Denver",
            "layoverMinutes": 65
          }
        ],
        "price": {
          "amount": 189,
          "currency": "USD"
        },
        "cabin": "economy",
        "badges": [
          "Cheapest"
        ],
        "notes": "Carry-on only; checked bags extra."
      },
      {
        "id": "ap9",
        "airline": "Aurora Pacific",
        "flightNumbers": [
          "AP 9"
        ],
        "depart": {
          "airport": "SFO",
          "city": "San Francisco",
          "timeZone": "America/Los_Angeles",
          "time": "2026-11-03T22:30:00-08:00"
        },
        "arrive": {
          "airport": "JFK",
          "city": "New York",
          "timeZone": "America/New_York",
          "time": "2026-11-04T07:05:00-05:00"
        },
        "stops": [],
        "price": {
          "amount": 412.5,
          "currency": "USD"
        },
        "cabin": "premium_economy",
        "notes": "Overnight flight with lie-flat seats."
      }
    ]
  }
}
```

### 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":"Choose your flight to New York","description":"Tuesday, November 3 · 1 adult · Prices include taxes.","branding":{"name":"Juniper Travel","accentColor":"#0b7a5c"},"page":{"type":"flight_picker","options":[{"id":"zq101","airline":"Skyward Air","flightNumbers":["ZQ 101"],"depart":{"airport":"SFO","city":"San Francisco","timeZone":"America/Los_Angeles","time":"2026-11-03T06:00:00-08:00"},"arrive":{"airport":"JFK","city":"New York","timeZone":"America/New_York","time":"2026-11-03T14:25:00-05:00"},"stops":[],"price":{"amount":219,"currency":"USD"},"cabin":"economy","badges":["Fastest"],"recommended":true,"recommendedReason":"Nonstop, and only $30 more than the cheapest"},{"id":"bf220","airline":"Bluefin Airlines","flightNumbers":["BF 220","BF 1408"],"depart":{"airport":"SFO","city":"San Francisco","timeZone":"America/Los_Angeles","time":"2026-11-03T09:10:00-08:00"},"arrive":{"airport":"JFK","city":"New York","timeZone":"America/New_York","time":"2026-11-03T19:55:00-05:00"},"stops":[{"airport":"DEN","city":"Denver","layoverMinutes":65}],"price":{"amount":189,"currency":"USD"},"cabin":"economy","badges":["Cheapest"],"notes":"Carry-on only; checked bags extra."},{"id":"ap9","airline":"Aurora Pacific","flightNumbers":["AP 9"],"depart":{"airport":"SFO","city":"San Francisco","timeZone":"America/Los_Angeles","time":"2026-11-03T22:30:00-08:00"},"arrive":{"airport":"JFK","city":"New York","timeZone":"America/New_York","time":"2026-11-04T07:05:00-05:00"},"stops":[],"price":{"amount":412.5,"currency":"USD"},"cabin":"premium_economy","notes":"Overnight flight with lie-flat seats."}]}}}'
```

## 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
```
