Skip to content

Models Reference

Core Models

FlightSearchFilters

The main model for configuring flight searches.

from fli.models import (
    FlightSearchFilters, FlightSegment, Airport,
    SeatType, MaxStops, SortBy, TripType, PassengerInfo
)

# Create flight segments for round trip
flight_segments = [
    FlightSegment(
        departure_airport=[[Airport.JFK, 0]],
        arrival_airport=[[Airport.LAX, 0]],
        travel_date="2026-06-01",
    ),
    FlightSegment(
        departure_airport=[[Airport.LAX, 0]],
        arrival_airport=[[Airport.JFK, 0]],
        travel_date="2026-06-15",
    )
]

filters = FlightSearchFilters(
    trip_type=TripType.ROUND_TRIP,
    passenger_info=PassengerInfo(adults=1),
    flight_segments=flight_segments,
    stops=MaxStops.NON_STOP,
    seat_type=SeatType.ECONOMY,
    sort_by=SortBy.CHEAPEST
)

Validation Rules: - Flight segments must have different departure and arrival airports - Travel dates cannot be in the past - For round trips, exactly two flight segments are required - Passenger counts must be valid (at least one adult)

fli.models.google_flights.FlightSearchFilters

Bases: BaseModel

Complete set of filters for flight search.

This model matches required Google Flights' API structure.

airlines: list[Airline] | None = None class-attribute instance-attribute

airlines_exclude: list[Airline] | None = None class-attribute instance-attribute

alliances: list[Alliance] | None = None class-attribute instance-attribute

alliances_exclude: list[Alliance] | None = None class-attribute instance-attribute

bags: BagsFilter | None = None class-attribute instance-attribute

emissions: EmissionsFilter = EmissionsFilter.ALL class-attribute instance-attribute

exclude_basic_economy: bool = False class-attribute instance-attribute

flight_segments: list[FlightSegment] instance-attribute

layover_restrictions: LayoverRestrictions | None = None class-attribute instance-attribute

max_duration: PositiveInt | None = None class-attribute instance-attribute

passenger_info: PassengerInfo instance-attribute

price_limit: PriceLimit | None = None class-attribute instance-attribute

seat_type: SeatType = SeatType.ECONOMY class-attribute instance-attribute

show_all_results: bool = True class-attribute instance-attribute

sort_by: SortBy = SortBy.BEST class-attribute instance-attribute

stops: MaxStops = MaxStops.ANY class-attribute instance-attribute

trip_type: TripType = TripType.ONE_WAY class-attribute instance-attribute

encode() -> str

URL encode the formatted filters for API request.

Source code in fli/models/google_flights/flights.py
def encode(self) -> str:
    """URL encode the formatted filters for API request."""
    formatted_filters = self.format()
    # First convert the formatted filters to a JSON string
    formatted_json = json.dumps(formatted_filters, separators=(",", ":"))
    # Then wrap it in a list with null
    wrapped_filters = [None, formatted_json]
    # Finally, encode the whole thing
    return urllib.parse.quote(json.dumps(wrapped_filters, separators=(",", ":")))

format() -> list

Format filters into Google Flights API structure.

This method converts the FlightSearchFilters model into the specific nested list/dict structure required by Google Flights' API.

The output format matches Google Flights' internal API structure, with careful handling of nested arrays and proper serialization of enums and model objects.

RETURNS DESCRIPTION
list

A formatted list structure ready for the Google Flights API request

TYPE: list

Source code in fli/models/google_flights/flights.py
def format(self) -> list:
    """Format filters into Google Flights API structure.

    This method converts the FlightSearchFilters model into the specific nested list/dict
    structure required by Google Flights' API.

    The output format matches Google Flights' internal API structure, with careful handling
    of nested arrays and proper serialization of enums and model objects.

    Returns:
        list: A formatted list structure ready for the Google Flights API request

    """

    def serialize(obj):
        if isinstance(obj, Airport) or isinstance(obj, Airline):
            return obj.name.removeprefix("_")
        if isinstance(obj, Enum):
            return obj.value
        if isinstance(obj, list):
            return [serialize(item) for item in obj]
        if isinstance(obj, dict):
            return {key: serialize(value) for key, value in obj.items()}
        if isinstance(obj, BaseModel):
            return serialize(obj.dict(exclude_none=True))
        return obj

    # Format flight segments. Google's UI classifies segments with a
    # trailing int at position 14:
    #   - 3 = outbound (first leg, or only leg of a one-way / multi-city)
    #   - 1 = return (second leg of a round-trip)
    # Empirically Google's `GetShoppingResults` accepts a uniform `3`
    # for both segments without errors, but `GetBookingResults`
    # rejects the request with INVALID_ARGUMENT unless the classifier
    # matches the UI's pattern (verified May 2026 by diffing the
    # browser's POST body against our own).
    formatted_segments = []
    for seg_idx, segment in enumerate(self.flight_segments):
        # Format airport codes with correct nesting
        segment_filters = [
            [
                [
                    [serialize(airport[0]), serialize(airport[1])]
                    for airport in segment.departure_airport
                ]
            ],
            [
                [
                    [serialize(airport[0]), serialize(airport[1])]
                    for airport in segment.arrival_airport
                ]
            ],
        ]

        # Time restrictions
        if segment.time_restrictions:
            time_filters = [
                segment.time_restrictions.earliest_departure,
                segment.time_restrictions.latest_departure,
                segment.time_restrictions.earliest_arrival,
                segment.time_restrictions.latest_arrival,
            ]
        else:
            time_filters = None

        # Airlines include — accepts a mix of airline IATA codes and
        # alliance identifier strings ("ONEWORLD" / "SKYTEAM" /
        # "STAR_ALLIANCE"). Sort airline codes for deterministic encoding;
        # append alliance strings after, also sorted, so the request body
        # is stable across runs (only matters for snapshot tests).
        airlines_filters: list | None = None
        include_tokens: list[str] = []
        if self.airlines:
            include_tokens.extend(
                serialize(a) for a in sorted(self.airlines, key=lambda x: x.value)
            )
        if self.alliances:
            include_tokens.extend(sorted(a.value for a in self.alliances))
        if include_tokens:
            airlines_filters = include_tokens

        # Airlines exclude — same dual-purpose list shape (codes + alliance
        # names), stored at segment[5]. Empirically discovered May 2026.
        exclude_filters: list | None = None
        exclude_tokens: list[str] = []
        if self.airlines_exclude:
            exclude_tokens.extend(
                serialize(a) for a in sorted(self.airlines_exclude, key=lambda x: x.value)
            )
        if self.alliances_exclude:
            exclude_tokens.extend(sorted(a.value for a in self.alliances_exclude))
        if exclude_tokens:
            exclude_filters = exclude_tokens

        # Layover restrictions
        layover_airports = (
            [serialize(a) for a in self.layover_restrictions.airports]
            if self.layover_restrictions and self.layover_restrictions.airports
            else None
        )
        layover_min_duration = (
            self.layover_restrictions.min_duration if self.layover_restrictions else None
        )
        layover_max_duration = (
            self.layover_restrictions.max_duration if self.layover_restrictions else None
        )

        # Selected flight (to fetch return/next-leg flights)
        selected_flights = None
        is_multi_leg = self.trip_type in (TripType.ROUND_TRIP, TripType.MULTI_CITY)
        if is_multi_leg and segment.selected_flight is not None:
            selected_flights = [
                [
                    serialize(leg.departure_airport),
                    serialize(leg.departure_datetime.strftime("%Y-%m-%d")),
                    serialize(leg.arrival_airport),
                    None,
                    serialize(leg.airline),
                    serialize(leg.flight_number),
                ]
                for leg in segment.selected_flight.legs
            ]

        # Emissions filter
        emissions_filter = (
            [self.emissions.value] if self.emissions != EmissionsFilter.ALL else None
        )

        # Segment classifier: 3 for outbound (or only leg), 1 for return.
        is_return = self.trip_type == TripType.ROUND_TRIP and seg_idx > 0
        classifier = 1 if is_return else 3

        segment_formatted = [
            segment_filters[0],  # 0: departure airport
            segment_filters[1],  # 1: arrival airport
            time_filters,  # 2: time restrictions [edep, ldep, earr, larr]
            serialize(self.stops.value),  # 3: stops int
            airlines_filters,  # 4: airline / alliance INCLUDE list
            exclude_filters,  # 5: airline / alliance EXCLUDE list
            segment.travel_date,  # 6: travel date
            [self.max_duration] if self.max_duration else None,  # 7: max duration
            selected_flights,  # 8: selected flight (next-leg fetch)
            layover_airports,  # 9: layover airport include list
            None,  # 10: ? (rejects scalars; no observed effect)
            layover_min_duration,  # 11: min layover duration (mins)
            layover_max_duration,  # 12: max layover duration (mins)
            emissions_filter,  # 13: emissions filter [1]=less emissions
            classifier,  # 14: classifier (3=outbound, 1=return)
        ]
        formatted_segments.append(segment_formatted)

    # Bags filter
    bags_filter = [self.bags.checked_bags, int(self.bags.carry_on)] if self.bags else None

    # The browser uses a wrapper nesting where outer[1] = [[], [main], ...fields...]
    # with self-transfer at wrapper[6] and basic economy at wrapper[15].
    # However, the wrapper format returns empty results through our API client
    # (likely requires browser cookies/headers). We use a flat format instead
    # which the API accepts. NOTE: Self-transfer cannot be toggled in the flat format.
    #
    # Main settings (filters[1]) index map:
    #   0:  unknown - seemingly no effect (tested 0-3, [], "en")
    #   1:  unknown - seemingly no effect (tested "USD"/"EUR"/"GBP"/"JPY");
    #       currency appears to be determined by IP/locale
    #   2:  trip type
    #   3:  unknown - seemingly no effect (tested 0-3)
    #   4:  unknown - seemingly no effect as [] or None; 400s on scalars
    #   5:  seat/cabin type
    #   6:  passenger counts [adults, children, infants_lap, infants_seat]
    #   7:  price limit [None, max_price]
    #   8:  unknown - seemingly no effect (tested 0-3, arrays)
    #   9:  unknown - seemingly no effect (tested 0-3, arrays)
    #   10: bags filter [checked_bags, carry_on]
    #   11: unknown - seemingly no effect (tested 0-3, arrays)
    #   12: unknown - seemingly no effect (tested 0-3, arrays)
    #   13: flight segments
    #   14-16: unknown - seemingly no effect
    #   17: unknown - seemingly no effect (hardcoded to 1)
    #   18-27: unknown - seemingly no effect
    #   28: exclude basic economy (0=allow, 1=exclude)
    #
    filters = [
        [],  # outer[0]
        [
            None,  # [0] seemingly no effect
            None,  # [1] seemingly no effect (not currency)
            serialize(self.trip_type.value),
            None,  # [3] seemingly no effect
            [],  # [4] seemingly no effect
            serialize(self.seat_type.value),
            [
                self.passenger_info.adults,
                self.passenger_info.children,
                self.passenger_info.infants_on_lap,
                self.passenger_info.infants_in_seat,
            ],
            [None, self.price_limit.max_price] if self.price_limit else None,
            None,  # [8] seemingly no effect
            None,  # [9] seemingly no effect
            bags_filter,  # [10] bags filter [checked_bags, carry_on]
            None,  # [11] seemingly no effect
            None,  # [12] seemingly no effect
            formatted_segments,
            None,  # [14] seemingly no effect
            None,  # [15] seemingly no effect
            None,  # [16] seemingly no effect
            1,  # [17] seemingly no effect (hardcoded to 1)
            None,  # [18] seemingly no effect
            None,  # [19] seemingly no effect
            None,  # [20] seemingly no effect
            None,  # [21] seemingly no effect
            None,  # [22] seemingly no effect
            None,  # [23] seemingly no effect
            None,  # [24] seemingly no effect
            None,  # [25] seemingly no effect
            None,  # [26] seemingly no effect
            None,  # [27] seemingly no effect
            1 if self.exclude_basic_economy else 0,
        ],
        serialize(self.sort_by.value),  # outer[2] sort mode
        1 if self.show_all_results else 0,  # outer[3] 0=~30, 1=all results
        0,  # outer[4] seemingly no effect
        1,  # outer[5] seemingly no effect
    ]

    return filters

FlightResult

Represents a flight search result with complete details.

fli.models.google_flights.FlightResult

Bases: BaseModel

Complete flight search result with pricing and timing.

price is None when Google did not surface a per-row aggregate price in the shopping response. This happens predictably for premium-cabin (BUSINESS / FIRST) round-trip itineraries with multi-passenger configs — Google expects the caller to pick a specific outbound+return pair and fetch real fares via :meth:SearchFlights.get_booking_options. The per-row booking_token is still populated in that case, so the booking follow-up has everything it needs.

Downstream code that filters or sorts by price should guard against None — the convenience property :attr:price_unknown exists for that purpose (if flight.price_unknown: ... reads more cleanly than if flight.price is None: ...).

booking_token: str | None = None class-attribute instance-attribute

co2_emissions_delta_pct: int | None = None class-attribute instance-attribute

co2_emissions_g: NonNegativeInt | None = None class-attribute instance-attribute

co2_emissions_typical_g: NonNegativeInt | None = None class-attribute instance-attribute

currency: str | None = None class-attribute instance-attribute

duration: PositiveInt instance-attribute

emissions_tag: str | None = None class-attribute instance-attribute

layovers: list[Layover] | None = None class-attribute instance-attribute

legs: list[FlightLeg] instance-attribute

mixed_cabin: bool | None = None class-attribute instance-attribute

price: NonNegativeFloat | None = None class-attribute instance-attribute

price_unknown: bool property

True when Google did not surface a price for this row.

Equivalent to self.price is None — provided for readability in filtering / sorting code. Use this to skip priceless rows when computing aggregate statistics::

cheapest = min(
    (f for f in flights if not f.price_unknown),
    key=lambda f: f.price,
    default=None,
)

See :issue:165 for the wire-format quirk that motivates this.

primary_airline: Airline | None = None class-attribute instance-attribute

primary_airline_name: str | None = None class-attribute instance-attribute

self_transfer: bool | None = None class-attribute instance-attribute

stops: NonNegativeInt instance-attribute

FlightLeg

Represents a single flight segment with airline and timing details.

fli.models.google_flights.FlightLeg

Bases: BaseModel

A single flight leg (segment) with airline and timing details.

aircraft: str | None = None class-attribute instance-attribute

airline: Airline instance-attribute

amenities: Amenities | None = None class-attribute instance-attribute

arrival_airport: Airport instance-attribute

arrival_airport_name: str | None = None class-attribute instance-attribute

arrival_datetime: datetime instance-attribute

co2_emissions_g: NonNegativeInt | None = None class-attribute instance-attribute

departure_airport: Airport instance-attribute

departure_airport_name: str | None = None class-attribute instance-attribute

departure_datetime: datetime instance-attribute

duration: PositiveInt instance-attribute

flight_number: str instance-attribute

legroom: str | None = None class-attribute instance-attribute

legroom_short: str | None = None class-attribute instance-attribute

operating_airline: Airline | None = None class-attribute instance-attribute

operating_flight_number: str | None = None class-attribute instance-attribute

overnight: bool = False class-attribute instance-attribute

Enums

SeatType

Available cabin classes for flights.

fli.models.google_flights.SeatType

Bases: Enum

Available cabin classes for flights.

BUSINESS = 3 class-attribute instance-attribute

ECONOMY = 1 class-attribute instance-attribute

FIRST = 4 class-attribute instance-attribute

PREMIUM_ECONOMY = 2 class-attribute instance-attribute

MaxStops

Maximum number of stops allowed in flight search.

fli.models.google_flights.MaxStops

Bases: Enum

Maximum number of stops allowed in flight search.

ANY = 0 class-attribute instance-attribute

NON_STOP = 1 class-attribute instance-attribute

ONE_STOP_OR_FEWER = 2 class-attribute instance-attribute

TWO_OR_FEWER_STOPS = 3 class-attribute instance-attribute

SortBy

Available sorting options for flight results.

fli.models.google_flights.SortBy

Bases: Enum

Available sorting options for flight results.

Maps to the top-level sort_mode value in the Google Flights API payload.

ARRIVAL_TIME = 4 class-attribute instance-attribute

BEST = 1 class-attribute instance-attribute

CHEAPEST = 2 class-attribute instance-attribute

DEPARTURE_TIME = 3 class-attribute instance-attribute

DURATION = 5 class-attribute instance-attribute

EMISSIONS = 6 class-attribute instance-attribute

TOP_FLIGHTS = 0 class-attribute instance-attribute

TripType

Type of trip for flight search.

fli.models.google_flights.TripType

Bases: Enum

Type of flight journey.

MULTI_CITY = 3 class-attribute instance-attribute

ONE_WAY = 2 class-attribute instance-attribute

ROUND_TRIP = 1 class-attribute instance-attribute

Support Models

PassengerInfo

Configuration for passenger counts.

fli.models.google_flights.PassengerInfo

Bases: BaseModel

Passenger configuration for flight search.

adults: NonNegativeInt = 1 class-attribute instance-attribute

children: NonNegativeInt = 0 class-attribute instance-attribute

infants_in_seat: NonNegativeInt = 0 class-attribute instance-attribute

infants_on_lap: NonNegativeInt = 0 class-attribute instance-attribute

TimeRestrictions

Time constraints for flight departure and arrival.

fli.models.google_flights.TimeRestrictions

Bases: BaseModel

Time constraints for flight departure and arrival in local time.

All times are in hours from midnight (e.g., 20 = 8:00 PM).

earliest_arrival: NonNegativeInt | None = None class-attribute instance-attribute

earliest_departure: NonNegativeInt | None = None class-attribute instance-attribute

latest_arrival: PositiveInt | None = None class-attribute instance-attribute

latest_departure: PositiveInt | None = None class-attribute instance-attribute

validate_latest_times(v: PositiveInt | None, info: ValidationInfo) -> PositiveInt | None classmethod

Validate and adjust the latest time restrictions.

Source code in fli/models/google_flights/base.py
@field_validator("latest_departure", "latest_arrival")
@classmethod
def validate_latest_times(
    cls, v: PositiveInt | None, info: ValidationInfo
) -> PositiveInt | None:
    """Validate and adjust the latest time restrictions."""
    if v is None:
        return v

    # Get "departure" or "arrival" from field name
    field_prefix = "earliest_" + info.field_name[7:]
    earliest = info.data.get(field_prefix)

    # Swap values to ensure that `from` is always before `to`
    if earliest is not None and earliest > v:
        info.data[field_prefix] = v
        return earliest
    return v

PriceLimit

Price constraints for flight search.

fli.models.google_flights.PriceLimit

Bases: BaseModel

Maximum price constraint for flight search.

currency: Currency | None = Currency.USD class-attribute instance-attribute

max_price: PositiveInt instance-attribute