gflights 0.1.0

Unofficial async Rust client for the Google Flights web API — search flights, price graphs, and booking offers.
Documentation

gflights

Unofficial async Rust client for the Google Flights web API.

Search flights, compare prices across a date range, retrieve booking offers, and resolve booking URLs — all without an official API key.

Disclaimer: This library talks to the same endpoints used by the Google Flights website. It is not affiliated with or endorsed by Google. Usage is subject to Google's Terms of Service.


Features

  • Flight search — one-way, return, multi-stop itineraries
  • Price graph — cheapest fares across a configurable date range
  • Date grid — full departure × return price matrix for round trips
  • Booking offers — airline/OTA offers with prices and booking URLs
  • City / airport lookup — resolve city names and IATA codes
  • Multi-airport search — up to 4 departure or destination airports
  • Airline / alliance filters — include or exclude specific airlines or alliances (oneworld, SkyTeam, Star Alliance)
  • Connection filters — require layover through specific airports; set min/max layover duration
  • Lower-emissions filter — restrict to flights with below-average CO₂
  • Locale supportlanguage + country for non-English results
  • Sort order — Best · Price · Duration · Departure time · Arrival time
  • CO2 / emissions — included in parsed itinerary data
  • Layover details — connection time, airport codes, overnight warnings
  • Rate limiting — built-in governor-based token-bucket limiter
  • Retry logic — exponential back-off for transient 5xx / timeout errors
  • CLI — interactive REPL and one-shot subcommands (search, graph, dgrid, offer)

Installation

[dependencies]
gflights = "0.1.0"
tokio = { version = "1", features = ["full"] }

CLI

The crate ships a gflights binary. Install it with:

cargo install gflights

One-shot mode

# Search flights
gflights search --from LHR --to JFK --date 2026-08-01

# Round trip with filters
gflights search --from MXP --to NRT --date 2026-09-01 --return 2026-09-15 \
  --airline LX --airline ONEWORLD --via ZRH \
  --min-layover 60 --max-layover 180 \
  --lower-emissions --sort price --format json

# Price graph (cheapest fare per day over 3 months)
gflights graph --from LHR --to JFK --date 2026-08-01 --months 3

# Departure × return price grid
gflights dgrid --from LHR --to JFK \
  --dep-start 2026-08-01 --dep-end 2026-08-07 \
  --ret-start 2026-08-15 --ret-end 2026-08-22

# Booking offers with clickable URLs (OSC 8, supported in most modern terminals)
gflights offer --from FRA --to SIN --date 2026-10-01

Interactive REPL

Run gflights with no arguments to enter an interactive session with history:

gflights> search --from LHR --to JFK --date 2026-08-01
gflights> graph  --from MXP --to SYD --date 2026-09-01 --months 2
gflights> dgrid  --from LHR --to JFK --dep-start 2026-08-01 --dep-end 2026-08-07 --ret-start 2026-08-15 --ret-end 2026-08-22
gflights> quit

search flag reference

Flag Default Description
--from <CODE> required Departure airport IATA code or city name
--to <CODE> required Destination airport IATA code or city name
--date <YYYY-MM-DD> required Outbound departure date
--return <YYYY-MM-DD> one-way Return date
--adults <N> 1 Number of adult passengers
--class <CLASS> economy economy · premium-economy · business · first
--stops <STOPS> all all · non-stop · one-stop
--sort <SORT> best best · price · duration · departure-time · arrival-time ¹
--airline <CODE> Include airline IATA code or alliance (ONEWORLD, SKYTEAM, STAR_ALLIANCE). Repeatable.
--exclude-airline <CODE> Exclude airline or alliance. Repeatable.
--via <IATA> Require connection through this airport. Repeatable.
--min-layover <MINS> none Minimum layover in minutes (rounded up to 30 min intervals)
--max-layover <MINS> none Maximum layover in minutes
--lower-emissions off Restrict to below-average CO₂ flights
--currency <CURRENCY> euro Result currency (e.g. us-dollar, british-pound)
--lang <CODE> en BCP-47 language subtag
--country <CODE> GB ISO 3166-1 alpha-2 country code
--format <FORMAT> table table · json

¹ departure-time and arrival-time are sorted client-side after Google returns results.

dgrid flag reference

Flag Default Description
--from <CODE> required Departure airport IATA code or city name
--to <CODE> required Destination airport IATA code or city name
--dep-start <DATE> required First outbound departure date
--dep-end <DATE> required Last outbound departure date
--ret-start <DATE> required First return date
--ret-end <DATE> required Last return date
--adults <N> 1 Number of adult passengers
--class <CLASS> economy Travel class
--stops <STOPS> all Stop filter
--currency <CURRENCY> euro Result currency
--format <FORMAT> table table · json

Quick start

Search for flights

use gflights::requests::{api::ApiClient, config::Config};
use chrono::{Duration, Utc};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = ApiClient::new().await;
    let today = Utc::now().date_naive();

    let config = Config::builder()
        .departure("LHR", &client).await?   // London Heathrow
        .destination("JFK", &client).await? // New York JFK
        .departing_date(today + Duration::days(14))
        .return_date(today + Duration::days(21))
        .build()?;

    let results = client.request_flights(&config).await?;

    for resp in &results.responses {
        if let Some(flights) = resp.maybe_get_all_flights() {
            for f in &flights {
                println!(
                    "{}{}h{}m — stops: {}{:?}",
                    f.itinerary.flight_by,
                    f.itinerary.total_time_minutes / 60,
                    f.itinerary.total_time_minutes % 60,
                    f.itinerary.stop_count(),
                    f.itinerary_cost.trip_cost,
                );
            }
        }
    }
    Ok(())
}

Run the full worked example:

cargo run --example flights

Price graph across a date range

use gflights::requests::{api::ApiClient, config::{Config, Currency}};
use chrono::{Duration, Months, Utc};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = ApiClient::new().await;
    let today = Utc::now().date_naive();

    let config = Config::builder()
        .departure("MAD", &client).await?
        .destination("MEX", &client).await?
        .departing_date(today + Duration::days(10))
        .currency(Currency::USDollar)
        .build()?;

    let graph = client.request_graph(&config, Months::new(3)).await?;

    if let Some((date, price)) = graph
        .get_all_graphs()
        .iter()
        .filter_map(|g| g.maybe_get_date_price())
        .min_by_key(|&(_, p)| p)
    {
        println!("Cheapest: {} at ${:.2}", date, price);
    }
    Ok(())
}

Run with:

cargo run --example graph

Configuration reference

Builder method Type Default Description
.departure(iata, &client) async &str required Departure airport / city
.destination(iata, &client) async &str required Destination airport / city
.departure_location(loc) Location Set departure from existing Location (no network)
.destination_location(loc) Location Set destination from existing Location
.add_departure(iata, &client) async &str Add extra departure (max 4)
.add_destination(iata, &client) async &str Add extra destination (max 4)
.departing_date(date) NaiveDate required Outbound departure date
.return_date(date) NaiveDate one-way Return date (omit for one-way)
.travelers(t) Travelers 1 adult Passenger counts
.travel_class(c) TravelClass Economy Economy / Business / First
.stop_options(s) StopOptions Any Non-stop · Max1 · Any
.sort_order(s) SortOrder Best Best · Price · Duration · DepartureTime · ArrivalTime
.currency(c) Currency EUR Result currency
.language(s) &str "en" BCP-47 language subtag
.country(s) &str "GB" ISO 3166-1 alpha-2 country code
.stopover_min(d) StopoverDuration Unlimited Minimum layover duration
.stopover_max(d) StopoverDuration Unlimited Maximum layover duration
.duration_max(d) TotalDuration Unlimited Maximum total trip duration
.departing_times(t) FlightTimes Any Outbound departure time window
.return_times(t) FlightTimes Any Return departure time window
.airlines_include(v) Vec<AirlineFilter> none Restrict to these airlines / alliances
.add_airline_include(f) AirlineFilter Add one airline / alliance to include filter
.airlines_exclude(v) Vec<AirlineFilter> none Exclude these airlines / alliances
.add_airline_exclude(f) AirlineFilter Add one airline / alliance to exclude filter
.connecting_airports(v) Vec<String> none Require connection through these IATA airport codes
.add_connecting_airport(s) &str Add one connecting airport
.lower_emissions(b) bool false Restrict to below-average CO₂ flights

Travelers

use gflights::parsers::common::Travelers;

// [adults, children, infants_in_seat, infants_on_lap]
let travelers = Travelers::new(vec![2, 1, 0, 0])?; // 2 adults + 1 child

Rules: at least 1 adult, total ≤ 9 passengers.

Airline & connection filters

use gflights::parsers::common::{AirlineFilter, Alliance};

let config = Config::builder()
    .departure("LHR", &client).await?
    .destination("JFK", &client).await?
    .departing_date(date)
    // Only show British Airways and oneworld alliance members
    .add_airline_include("BA".parse::<AirlineFilter>()?)
    .add_airline_include(AirlineFilter::Alliance(Alliance::OneWorld))
    // Must connect through Dublin
    .add_connecting_airport("DUB")
    // At least 45 min layover, at most 3 hours
    .stopover_min(StopoverDuration::Minutes(45))
    .stopover_max(StopoverDuration::Minutes(180))
    // Lower CO₂ only
    .lower_emissions(true)
    .build()?;

Rate limiting

ApiClient uses a governor token-bucket rate limiter (default: 10 req/s).

use gflights::requests::api::ApiClient;
use governor::Quota;
use std::num::NonZeroU32;

// Custom: 2 requests per second
let quota = Quota::per_second(NonZeroU32::new(2).unwrap());
let client = ApiClient::new_with_ratelimit(quota).await;

If Google returns HTTP 429, the client sets an internal flag and all subsequent requests immediately return RateLimitedError without touching the network. Reset it after a cooling-off period:

if client.is_rate_limited() {
    tokio::time::sleep(std::time::Duration::from_secs(60)).await;
    client.reset_rate_limit();
}

Retry logic

Transient server errors (HTTP 500/502/503/504) and connection timeouts are automatically retried with exponential back-off. Defaults: 3 attempts, 500 ms base delay, 30 s cap.

use gflights::{requests::api::ApiClient, RetryConfig};

let client = ApiClient::new().await
    .with_retry_config(RetryConfig {
        max_attempts: 5,
        base_delay_ms: 200,
        cap_delay_ms: 10_000,
    });

Set max_attempts: 1 to disable retries entirely.


Error handling

All public async methods return anyhow::Result<T>. Downcast RateLimitedError to check for 429:

use gflights::RateLimitedError;

match client.request_flights(&config).await {
    Ok(resp) => { /* use resp */ }
    Err(e) if e.downcast_ref::<RateLimitedError>().is_some() => {
        eprintln!("Rate limited — back off and retry");
    }
    Err(e) => eprintln!("Other error: {e}"),
}

Known limitations

x-goog-batchexecute-bgr header — computed deep in Google's obfuscated JS from the current time and request payload length — is omitted. Responses are still valid but may occasionally be less accurate (e.g. missing low-fare calendar data). Contributions to reverse-engineer the algorithm are welcome.


Development

# Build
cargo build

# Unit tests (no network)
cargo test --lib

# Binary (CLI) tests
cargo test --bin gflights

# Doc tests
cargo test --doc

# Live integration tests (requires internet, skipped in CI)
RUN_LIVE_TESTS=1 cargo test --lib -- --ignored

# Docs
cargo doc --open

# Lint
cargo clippy --all-targets -- -D warnings

# Format
cargo fmt

# Security audit
cargo audit