Skip to content

Search API Reference

The main search functionality for finding specific flights.

SearchFlights

fli.search.flights.SearchFlights()

Flight search implementation using Google Flights' API.

This class handles searching for specific flights with detailed filters, parsing the results into structured data models.

Initialize the search client for flight searches.

Source code in fli/search/flights.py
def __init__(self):
    """Initialize the search client for flight searches."""
    self.client = get_client()

BASE_URL = 'https://www.google.com/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetShoppingResults' class-attribute instance-attribute

DEFAULT_HEADERS = {'content-type': 'application/x-www-form-urlencoded;charset=UTF-8'} class-attribute instance-attribute

client = get_client() instance-attribute

search(filters: FlightSearchFilters, top_n: int = 5) -> list[FlightResult | tuple[FlightResult, ...]] | None

Search for flights using the given FlightSearchFilters.

PARAMETER DESCRIPTION
filters

Full flight search object including airports, dates, and preferences

TYPE: FlightSearchFilters

top_n

Number of flights to limit the return flight search to

TYPE: int DEFAULT: 5

RETURNS DESCRIPTION
list[FlightResult | tuple[FlightResult, ...]] | None

List of FlightResult objects (one-way), tuples of FlightResult (round-trip

list[FlightResult | tuple[FlightResult, ...]] | None

or multi-city), or None if no results

RAISES DESCRIPTION
Exception

If the search fails or returns invalid data

Note

Multi-city searches (TripType.MULTI_CITY) with distinct city pairs may time out due to limitations of the Google Flights API endpoint. The endpoint reliably supports one-way and round-trip searches.

Source code in fli/search/flights.py
def search(
    self, filters: FlightSearchFilters, top_n: int = 5
) -> list[FlightResult | tuple[FlightResult, ...]] | None:
    """Search for flights using the given FlightSearchFilters.

    Args:
        filters: Full flight search object including airports, dates, and preferences
        top_n: Number of flights to limit the return flight search to

    Returns:
        List of FlightResult objects (one-way), tuples of FlightResult (round-trip
        or multi-city), or None if no results

    Raises:
        Exception: If the search fails or returns invalid data

    Note:
        Multi-city searches (TripType.MULTI_CITY) with distinct city pairs may
        time out due to limitations of the Google Flights API endpoint.  The
        endpoint reliably supports one-way and round-trip searches.

    """
    encoded_filters = filters.encode()

    try:
        response = self.client.post(
            url=self.BASE_URL,
            data=f"f.req={encoded_filters}",
            impersonate="chrome",
            allow_redirects=True,
        )
        response.raise_for_status()

        parsed = json.loads(response.text.lstrip(")]}'"))[0][2]
        if not parsed:
            return None

        encoded_filters = json.loads(parsed)
        flights_data = [
            item
            for i in [2, 3]
            if isinstance(encoded_filters[i], list)
            for item in encoded_filters[i][0]
        ]
        flights = [self._parse_flights_data(flight) for flight in flights_data]

        if filters.trip_type == TripType.ONE_WAY:
            return flights

        # For round-trip and multi-city, iteratively select each leg
        # and fetch the next leg's options with combined pricing.
        num_segments = len(filters.flight_segments)
        selected_count = sum(
            1 for s in filters.flight_segments if s.selected_flight is not None
        )

        # If all previous segments are selected, we're on the last leg
        if selected_count >= num_segments - 1:
            return flights

        # Select each flight option and fetch the next leg
        flight_combos = []
        for selected_flight in flights[:top_n]:
            next_filters = deepcopy(filters)
            next_filters.flight_segments[selected_count].selected_flight = selected_flight
            next_results = self.search(next_filters, top_n=top_n)
            if next_results is not None:
                for next_result in next_results:
                    if isinstance(next_result, tuple):
                        flight_combos.append((selected_flight,) + next_result)
                    else:
                        flight_combos.append((selected_flight, next_result))

        return flight_combos

    except Exception as e:
        raise Exception(f"Search failed: {str(e)}") from e

FlightSearchFilters

A simplified interface for flight search parameters.

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

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
        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
    formatted_segments = []
    for segment in 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
        airlines_filters = None
        if self.airlines:
            sorted_airlines = sorted(self.airlines, key=lambda x: x.value)
            airlines_filters = [serialize(airline) for airline in sorted_airlines]

        # 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_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.name),
                    serialize(leg.departure_datetime.strftime("%Y-%m-%d")),
                    serialize(leg.arrival_airport.name),
                    None,
                    serialize(leg.airline.name),
                    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_formatted = [
            segment_filters[0],  # departure airport
            segment_filters[1],  # arrival airport
            time_filters,  # time restrictions
            serialize(self.stops.value),  # stops
            airlines_filters,  # airlines
            None,  # unknown: accepts [] but 400s on scalars; seemingly no effect
            segment.travel_date,  # travel date
            [self.max_duration] if self.max_duration else None,  # max duration
            selected_flights,  # selected flight (to fetch return flights)
            layover_airports,  # layover airports
            None,  # unknown: accepts [] but 400s on scalars; seemingly no effect
            None,  # seemingly no effect: accepts any value (0-3, bool) without changing results
            layover_duration,  # layover duration
            emissions_filter,  # emissions filter: [1]=less emissions
            3,  # seemingly no effect: accepts any value (0-5, None) without changing results
        ]
        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

Search functionality for finding the cheapest dates to fly.

SearchDates

fli.search.dates.SearchDates()

Date-based flight search implementation.

This class provides methods to search for flight prices across a date range, useful for finding the cheapest dates to fly.

Initialize the search client for date-based searches.

Source code in fli/search/dates.py
def __init__(self):
    """Initialize the search client for date-based searches."""
    self.client = get_client()

BASE_URL = 'https://www.google.com/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetCalendarGraph' class-attribute instance-attribute

DEFAULT_HEADERS = {'content-type': 'application/x-www-form-urlencoded;charset=UTF-8'} class-attribute instance-attribute

client = get_client() instance-attribute

__parse_currency(item: list[list] | list | None) -> str | None staticmethod

Parse the returned currency code from the API response.

Source code in fli/search/dates.py
@staticmethod
def __parse_currency(item: list[list] | list | None) -> str | None:
    """Parse the returned currency code from the API response."""
    try:
        if item and isinstance(item, list) and len(item) > 2:
            if isinstance(item[2], list) and len(item[2]) > 1:
                return extract_currency_from_price_token(item[2][1])
    except (IndexError, TypeError, ValueError):
        pass

    return None

__parse_date(item: list[list] | list | None, trip_type: TripType) -> tuple[datetime] | tuple[datetime, datetime] staticmethod

Parse date data from the API response.

PARAMETER DESCRIPTION
item

Raw date data from the API response

TYPE: list[list] | list | None

trip_type

Trip type (one-way or round-trip)

TYPE: TripType

RETURNS DESCRIPTION
tuple[datetime] | tuple[datetime, datetime]

Tuple of datetime objects

Source code in fli/search/dates.py
@staticmethod
def __parse_date(
    item: list[list] | list | None, trip_type: TripType
) -> tuple[datetime] | tuple[datetime, datetime]:
    """Parse date data from the API response.

    Args:
        item: Raw date data from the API response
        trip_type: Trip type (one-way or round-trip)

    Returns:
        Tuple of datetime objects

    """
    if trip_type == TripType.ONE_WAY:
        return (datetime.strptime(item[0], "%Y-%m-%d"),)
    else:
        return (
            datetime.strptime(item[0], "%Y-%m-%d"),
            datetime.strptime(item[1], "%Y-%m-%d"),
        )

__parse_price(item: list[list] | list | None) -> float | None staticmethod

Parse price data from the API response.

PARAMETER DESCRIPTION
item

Raw price data from the API response

TYPE: list[list] | list | None

RETURNS DESCRIPTION
float | None

Float price value if valid, None if invalid or missing

Source code in fli/search/dates.py
@staticmethod
def __parse_price(item: list[list] | list | None) -> float | None:
    """Parse price data from the API response.

    Args:
        item: Raw price data from the API response

    Returns:
        Float price value if valid, None if invalid or missing

    """
    try:
        if item and isinstance(item, list) and len(item) > 2:
            if isinstance(item[2], list) and len(item[2]) > 0:
                if isinstance(item[2][0], list) and len(item[2][0]) > 1:
                    return float(item[2][0][1])
    except (IndexError, TypeError, ValueError):
        pass

    return None

search(filters: DateSearchFilters) -> list[DatePrice] | None

Search for flight prices across a date range and search parameters.

PARAMETER DESCRIPTION
filters

Search parameters including date range, airports, and preferences

TYPE: DateSearchFilters

RETURNS DESCRIPTION
list[DatePrice] | None

List of DatePrice objects containing date and price pairs, or None if no results

RAISES DESCRIPTION
Exception

If the search fails or returns invalid data

Notes
  • For date ranges larger than 61 days, splits into multiple searches.
  • We can't search more than 305 days in the future.
Source code in fli/search/dates.py
def search(self, filters: DateSearchFilters) -> list[DatePrice] | None:
    """Search for flight prices across a date range and search parameters.

    Args:
        filters: Search parameters including date range, airports, and preferences

    Returns:
        List of DatePrice objects containing date and price pairs, or None if no results

    Raises:
        Exception: If the search fails or returns invalid data

    Notes:
        - For date ranges larger than 61 days, splits into multiple searches.
        - We can't search more than 305 days in the future.

    """
    from_date = datetime.strptime(filters.from_date, "%Y-%m-%d")
    to_date = datetime.strptime(filters.to_date, "%Y-%m-%d")
    date_range = (to_date - from_date).days + 1

    if date_range <= self.MAX_DAYS_PER_SEARCH:
        return self._search_chunk(filters)

    # Split into chunks of MAX_DAYS_PER_SEARCH
    all_results = []
    current_from = from_date
    while current_from <= to_date:
        current_to = min(current_from + timedelta(days=self.MAX_DAYS_PER_SEARCH - 1), to_date)

        # Update the travel date for the flight segments
        if current_from > from_date:
            for segment in filters.flight_segments:
                segment.travel_date = (
                    datetime.strptime(segment.travel_date, "%Y-%m-%d")
                    + timedelta(days=self.MAX_DAYS_PER_SEARCH)
                ).strftime("%Y-%m-%d")

        # Create new filters for this chunk
        chunk_filters = DateSearchFilters(
            trip_type=filters.trip_type,
            passenger_info=filters.passenger_info,
            flight_segments=filters.flight_segments,
            stops=filters.stops,
            seat_type=filters.seat_type,
            airlines=filters.airlines,
            from_date=current_from.strftime("%Y-%m-%d"),
            to_date=current_to.strftime("%Y-%m-%d"),
            duration=filters.duration,
        )

        chunk_results = self._search_chunk(chunk_filters)
        if chunk_results:
            all_results.extend(chunk_results)

        current_from = current_to + timedelta(days=1)

    return all_results if all_results else None

DatePrice

fli.search.dates.DatePrice

Bases: BaseModel

Flight price for a specific date.

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

date: tuple[datetime] | tuple[datetime, datetime] instance-attribute

price: float instance-attribute

Examples

from fli.search import SearchFlights
from fli.models import Airport, SeatType, FlightSearchFilters, FlightSegment, PassengerInfo

# Create filters
filters = FlightSearchFilters(
    passenger_info=PassengerInfo(adults=1),
    flight_segments=[
        FlightSegment(
            departure_airport=[[Airport.JFK, 0]],
            arrival_airport=[[Airport.LAX, 0]],
            travel_date="2026-06-01",
        )
    ],
    seat_type=SeatType.ECONOMY
)

# Search flights
search = SearchFlights()
results = search.search(filters)
from fli.search import SearchDates
from fli.models import DateSearchFilters, Airport, FlightSegment, PassengerInfo

# Create filters
filters = DateSearchFilters(
    passenger_info=PassengerInfo(adults=1),
    flight_segments=[
        FlightSegment(
            departure_airport=[[Airport.JFK, 0]],
            arrival_airport=[[Airport.LAX, 0]],
            travel_date="2026-06-01",
        )
    ],
    from_date="2026-06-01",
    to_date="2026-06-30"
)

# Search dates
search = SearchDates()
results = search.search(filters)

Running These Examples

You can find complete, runnable versions of these examples in the examples/ directory:

# Run with uv (recommended)
uv run python examples/basic_one_way_search.py
uv run python examples/date_range_search.py

# Or install dependencies and run directly
pip install pydantic curl_cffi httpx
python examples/basic_one_way_search.py

For more advanced examples, see:

  • examples/complex_flight_search.py - Advanced filtering
  • examples/result_processing.py - Data analysis
  • examples/error_handling_with_retries.py - Robust error handling

💡 All examples include automatic dependency checking and helpful error messages.

HTTP Client

The underlying HTTP client used for API requests.

Client

fli.search.client.Client()

HTTP client with built-in rate limiting, retry and user agent impersonation functionality.

Initialize a new client session with default headers.

Source code in fli/search/client.py
def __init__(self):
    """Initialize a new client session with default headers."""
    self._client = requests.Session()
    self._client.headers.update(self.DEFAULT_HEADERS)

DEFAULT_HEADERS = {'content-type': 'application/x-www-form-urlencoded;charset=UTF-8'} class-attribute instance-attribute

__del__()

Clean up client session on deletion.

Source code in fli/search/client.py
def __del__(self):
    """Clean up client session on deletion."""
    if hasattr(self, "_client"):
        self._client.close()

get(url: str, **kwargs: Any) -> requests.Response

Make a rate-limited GET request with automatic retries.

PARAMETER DESCRIPTION
url

Target URL for the request

TYPE: str

**kwargs

Additional arguments passed to requests.get()

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Response

Response object from the server

RAISES DESCRIPTION
Exception

If request fails after all retries

Source code in fli/search/client.py
@sleep_and_retry
@limits(calls=10, period=1)
@retry(stop=stop_after_attempt(3), wait=wait_exponential(), reraise=True)
def get(self, url: str, **kwargs: Any) -> requests.Response:
    """Make a rate-limited GET request with automatic retries.

    Args:
        url: Target URL for the request
        **kwargs: Additional arguments passed to requests.get()

    Returns:
        Response object from the server

    Raises:
        Exception: If request fails after all retries

    """
    try:
        response = self._client.get(url, **kwargs)
        response.raise_for_status()
        return response
    except Exception as e:
        raise Exception(f"GET request failed: {str(e)}") from e

post(url: str, **kwargs: Any) -> requests.Response

Make a rate-limited POST request with automatic retries.

PARAMETER DESCRIPTION
url

Target URL for the request

TYPE: str

**kwargs

Additional arguments passed to requests.post()

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
Response

Response object from the server

RAISES DESCRIPTION
Exception

If request fails after all retries

Source code in fli/search/client.py
@sleep_and_retry
@limits(calls=10, period=1)
@retry(stop=stop_after_attempt(3), wait=wait_exponential(), reraise=True)
def post(self, url: str, **kwargs: Any) -> requests.Response:
    """Make a rate-limited POST request with automatic retries.

    Args:
        url: Target URL for the request
        **kwargs: Additional arguments passed to requests.post()

    Returns:
        Response object from the server

    Raises:
        Exception: If request fails after all retries

    """
    try:
        response = self._client.post(url, **kwargs)
        response.raise_for_status()
        return response
    except Exception as e:
        raise Exception(f"POST request failed: {str(e)}") from e