Skip to content

Search API Reference

The main search functionality for finding specific flights.

SearchFlights

fli.search.flights.SearchFlights()

Flight search via Google Flights' FlightsFrontendService API.

Public surface:

  • :meth:search — issue a GetShoppingResults call and return the parsed flights.
  • :meth:get_booking_options — follow up with GetBookingResults to surface bookable fares for a selected itinerary. See the method docstring for the live-token limitation.
Concurrency

SearchFlights is not thread-safe. The instance caches the shopping-session id from the most recent :meth:search call so :meth:get_booking_options can derive the booking token; two concurrent search calls on the same instance will race on that cache and may cross-pollinate sessions between unrelated bookings.

For multi-threaded or async server use, either (a) instantiate a fresh SearchFlights per request, or (b) pass the session_id returned by your own session bookkeeping into :meth:get_booking_options explicitly (the kwarg overrides the cached value).

Initialize the search client.

Source code in fli/search/flights.py
def __init__(self):
    """Initialize the search client."""
    self.client = get_client()
    # Last successful search response's ``inner[0][4]`` — the shopping
    # session id used to authenticate the follow-up GetBookingResults
    # call. Captured automatically by :meth:`search` so that
    # :meth:`get_booking_options` can derive the booking token without
    # the caller having to pass anything.
    self._last_session_id: str | None = None

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

BOOKING_URL = 'https://www.google.com/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetBookingResults' 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

get_booking_options(flight: FlightResult | tuple[FlightResult, ...], filters: FlightSearchFilters, currency: str | None = None, language: str | None = None, country: str | None = None, booking_token: str | None = None, session_id: str | None = None) -> list[BookingOption]

Fetch bookable fare options for a selected itinerary.

After a :meth:search call, the session id from Google's response is cached on the client and used here automatically — no explicit token plumbing is required by callers. The booking-call payload carries the same selected_flight legs the caller used in their round-trip search and a protobuf token constructed from the cached session id + the chosen itinerary's identifiers.

PARAMETER DESCRIPTION
flight

A :class:FlightResult (one-way) or tuple of results (round-trip / multi-city) from :meth:search.

TYPE: FlightResult | tuple[FlightResult, ...]

filters

The same filters used in the preceding :meth:search call. A copy is made internally; caller filters are not mutated.

TYPE: FlightSearchFilters

currency

Optional ISO 4217 currency code passed to Google as curr=. Also forms part of the booking token.

TYPE: str | None DEFAULT: None

language

Optional BCP-47 language code (hl URL param).

TYPE: str | None DEFAULT: None

country

Optional ISO 3166-1 alpha-2 country code (gl URL param).

TYPE: str | None DEFAULT: None

booking_token

Explicit override for outer[0][1]. Bypasses the automatic construction; use this when you have a token captured from a browser's tfu URL.

TYPE: str | None DEFAULT: None

session_id

Explicit override for the session id used to build the token. Defaults to the session captured by the most recent :meth:search call on this client.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
list[BookingOption]

A list of :class:BookingOption. Empty list when Google

list[BookingOption]

returns no vendors.

RAISES DESCRIPTION
ValueError

No session id available — either pass it explicitly or call :meth:search first.

Exception

HTTP request failure.

Source code in fli/search/flights.py
def get_booking_options(
    self,
    flight: FlightResult | tuple[FlightResult, ...],
    filters: FlightSearchFilters,
    currency: str | None = None,
    language: str | None = None,
    country: str | None = None,
    booking_token: str | None = None,
    session_id: str | None = None,
) -> list[BookingOption]:
    """Fetch bookable fare options for a selected itinerary.

    After a :meth:`search` call, the session id from Google's response
    is cached on the client and used here automatically — no explicit
    token plumbing is required by callers. The booking-call payload
    carries the same selected_flight legs the caller used in their
    round-trip search and a protobuf token constructed from the
    cached session id + the chosen itinerary's identifiers.

    Args:
        flight: A :class:`FlightResult` (one-way) or tuple of results
            (round-trip / multi-city) from :meth:`search`.
        filters: The same filters used in the preceding :meth:`search`
            call. A copy is made internally; caller filters are not
            mutated.
        currency: Optional ISO 4217 currency code passed to Google as
            ``curr=``. Also forms part of the booking token.
        language: Optional BCP-47 language code (``hl`` URL param).
        country: Optional ISO 3166-1 alpha-2 country code (``gl`` URL param).
        booking_token: Explicit override for ``outer[0][1]``.
            Bypasses the automatic construction; use this when you
            have a token captured from a browser's ``tfu`` URL.
        session_id: Explicit override for the session id used to build
            the token. Defaults to the session captured by the most
            recent :meth:`search` call on this client.

    Returns:
        A list of :class:`BookingOption`. Empty list when Google
        returns no vendors.

    Raises:
        ValueError: No session id available — either pass it
            explicitly or call :meth:`search` first.
        Exception: HTTP request failure.

    """
    results: list[FlightResult] = list(flight) if isinstance(flight, tuple) else [flight]
    if not results:
        raise ValueError("flight argument must be a FlightResult or non-empty tuple of them")

    # Resolve the session id: explicit > cached from prior search.
    effective_session = session_id or self._last_session_id

    token = booking_token
    if token is None and effective_session and results[-1].price is not None:
        # Build a session-anchored token from price + flight info.
        # Skipped when the last result has no shopping-list price
        # (premium-cabin round-trips often hit this) — the per-row
        # token from ``row[8]`` is the correct fallback there.
        from fli.search._proto import build_booking_token

        last = results[-1]
        last_leg = last.legs[-1]
        token = build_booking_token(
            session_id=effective_session,
            airline_code=last_leg.airline.name.removeprefix("_"),
            flight_number=last_leg.flight_number,
            leg_index=1,
            price_cents=int(last.price * 100),
            currency=last.currency or currency or "USD",
        )

    if token is None:
        # Fall back to the per-row token captured at parse time.
        #
        # Prefer the last result's token over the first because:
        #  - For one-way / single-segment trips they are the same row.
        #  - For round-trip / multi-city, ``row[8]`` on each result
        #    encodes the *full* itinerary at parse time (every leg,
        #    every flight number), so any row's token is sufficient
        #    to identify the booking — but using the last leg's
        #    matches Google's own indexing (``build_booking_token``
        #    above uses ``leg_index=1`` for the return leg) and is
        #    the row that ``get_booking_options`` is most likely to
        #    have just parsed if the caller is iterating return-leg
        #    candidates.
        #
        # Accessing the attribute directly fails loudly if the
        # caller passes a non-FlightResult, which is what we want.
        token = results[-1].booking_token or results[0].booking_token
    if not token:
        raise ValueError(
            "Missing booking token. Call SearchFlights.search(...) before "
            "get_booking_options(...) so the client can cache the session "
            "id, or pass `session_id` / `booking_token` explicitly. If "
            "your selected flight has ``price=None`` (premium-cabin "
            "round-trip rows often do — see issue #165), make sure its "
            "``booking_token`` attribute is set; the parser populates "
            "it from ``row[8]`` automatically."
        )

    prepared = deepcopy(filters)
    segments = prepared.flight_segments
    if len(results) > len(segments):
        raise ValueError(f"flight has {len(results)} segments but filters has {len(segments)}")
    for seg, res in zip(segments, results, strict=False):
        seg.selected_flight = res

    encoded = self._encode_booking_payload(token, prepared)
    url = with_locale_params(self.BOOKING_URL, currency, language, country)
    response = self.client.post(
        url=url,
        data=f"f.req={encoded}",
        impersonate="chrome",
        allow_redirects=True,
    )
    response.raise_for_status()

    # Booking responses are typically split into two wrb.fr chunks
    # (vendor list + price refinements). Materialise both before
    # parsing so we can parse them in parallel — each chunk is a few
    # hundred KB of pure-Python tree walking, GIL-bound but cheap to
    # overlap with the next chunk's JSON decode (which releases the GIL).
    chunks = list(iter_wrb_chunks(response.text))
    if not chunks:
        return []
    parsed = parallel_map(parse_booking_chunk, chunks)
    options: list[BookingOption] = []
    for chunk_options in parsed:
        options.extend(chunk_options)
    return options

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

Search for flights using the given :class:FlightSearchFilters.

PARAMETER DESCRIPTION
filters

Full search descriptor (airports, dates, preferences).

TYPE: FlightSearchFilters

top_n

Number of outbound options to expand when chasing a round-trip or multi-city itinerary.

TYPE: int DEFAULT: 5

currency

Optional ISO 4217 currency code (curr URL param).

TYPE: str | None DEFAULT: None

language

Optional BCP-47 language code (hl URL param).

TYPE: str | None DEFAULT: None

country

Optional ISO 3166-1 alpha-2 country code (gl URL param).

TYPE: str | None DEFAULT: None

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

For one-way trips, a list of :class:FlightResult. For

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

round-trip / multi-city, a list of tuples of

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

class:FlightResult (one per segment, in order). None

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

when no results.

RAISES DESCRIPTION
Exception

HTTP failure or unparseable response.

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

    Args:
        filters: Full search descriptor (airports, dates, preferences).
        top_n: Number of outbound options to expand when chasing a
            round-trip or multi-city itinerary.
        currency: Optional ISO 4217 currency code (``curr`` URL param).
        language: Optional BCP-47 language code (``hl`` URL param).
        country: Optional ISO 3166-1 alpha-2 country code (``gl`` URL param).

    Returns:
        For one-way trips, a list of :class:`FlightResult`. For
        round-trip / multi-city, a list of tuples of
        :class:`FlightResult` (one per segment, in order). ``None``
        when no results.

    Raises:
        Exception: HTTP failure or unparseable response.

    """
    flights = self._fetch_flights(
        filters,
        currency=currency,
        language=language,
        country=country,
        capture_session=True,
    )
    if flights is None:
        return None
    if filters.trip_type == TripType.ONE_WAY:
        return flights
    return self._expand_multi_leg(
        flights,
        filters,
        top_n=top_n,
        currency=currency,
        language=language,
        country=country,
    )

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

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

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, currency: str | None = None, language: str | None = None, country: str | None = None) -> 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

currency

Optional ISO 4217 currency code (e.g. "EUR") to bill prices in.

TYPE: str | None DEFAULT: None

language

Optional BCP-47 language code passed via the hl URL param.

TYPE: str | None DEFAULT: None

country

Optional ISO 3166-1 alpha-2 country code passed via the gl URL param.

TYPE: str | None DEFAULT: None

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,
    currency: str | None = None,
    language: str | None = None,
    country: str | None = None,
) -> list[DatePrice] | None:
    """Search for flight prices across a date range and search parameters.

    Args:
        filters: Search parameters including date range, airports, and preferences
        currency: Optional ISO 4217 currency code (e.g. ``"EUR"``) to bill prices in.
        language: Optional BCP-47 language code passed via the ``hl`` URL param.
        country: Optional ISO 3166-1 alpha-2 country code passed via the ``gl`` URL param.

    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, currency=currency, language=language, country=country
        )

    # Build every chunk descriptor up front so the per-chunk requests
    # share no mutable state. This both enables parallel execution and
    # fixes a latent bug in the previous sequential version: each chunk
    # rewrote ``filters.flight_segments[*].travel_date`` in place, so
    # the second-and-later chunks had segment dates that no longer
    # matched ``current_from``.
    chunk_filters = self._build_chunk_filters(filters, from_date, to_date)

    chunk_results = parallel_map(
        lambda cf: self._search_chunk(
            cf, currency=currency, language=language, country=country
        ),
        chunk_filters,
    )

    all_results: list[DatePrice] = []
    for r in chunk_results:
        if r:
            all_results.extend(r)
    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/python/ directory:

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

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

For more advanced examples, see:

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

HTTP Client

The underlying HTTP client used for API requests.

Client

fli.search.client.Client(calls_per_second: int = DEFAULT_CALLS_PER_SECOND)

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

Sessions are kept per-thread because curl_cffi.requests.Session is not thread-safe — concurrent post/get calls from different threads each get their own libcurl handle. The shared :class:TokenBucketRateLimiter enforces the global 10 req/sec budget across all of them.

Initialise the shared rate limiter and per-thread session storage.

Source code in fli/search/client.py
def __init__(
    self,
    calls_per_second: int = DEFAULT_CALLS_PER_SECOND,
):
    """Initialise the shared rate limiter and per-thread session storage."""
    self._sessions = threading.local()
    self._rate_limiter = TokenBucketRateLimiter(calls=calls_per_second, period=1.0)

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

__del__()

Best-effort cleanup of the main-thread session (others die with their thread).

Source code in fli/search/client.py
def __del__(self):
    """Best-effort cleanup of the main-thread session (others die with their thread)."""
    session = getattr(self._sessions, "session", None) if hasattr(self, "_sessions") else None
    if session is not None:
        try:
            session.close()
        except Exception:  # noqa: BLE001 — destruction-time best effort
            pass

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

Make a rate-limited GET request with automatic retries.

Source code in fli/search/client.py
@retry(stop=stop_after_attempt(3), wait=wait_exponential(), reraise=True)
def get(self, url: str, **kwargs: Any) -> Response:
    """Make a rate-limited GET request with automatic retries."""
    self._rate_limiter.acquire()
    kwargs.setdefault("timeout", REQUEST_TIMEOUT)
    try:
        response = self._session().get(url, **kwargs)
        response.raise_for_status()
        return response
    except Exception as e:
        raise _wrap_request_error("GET", url, e) from e

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

Make a rate-limited POST request with automatic retries.

Source code in fli/search/client.py
@retry(stop=stop_after_attempt(3), wait=wait_exponential(), reraise=True)
def post(self, url: str, **kwargs: Any) -> Response:
    """Make a rate-limited POST request with automatic retries."""
    self._rate_limiter.acquire()
    kwargs.setdefault("timeout", REQUEST_TIMEOUT)
    try:
        response = self._session().post(url, **kwargs)
        response.raise_for_status()
        return response
    except Exception as e:
        raise _wrap_request_error("POST", url, e) from e