# Google Flights Scraper & Price Monitor (`vincesoft/google-flights-price-monitor`) Actor

Collect Google Flights fares for travel apps and recurring price comparisons. Get paired round-trip totals, flight segments, booking links, and stable itinerary IDs. Batch up to 100 searches and export JSON or CSV. Schedule runs through Apify and compare saved results.

- **URL**: https://apify.com/vincesoft/google-flights-price-monitor.md
- **Developed by:** [VinceSoft](https://apify.com/vincesoft) (community)
- **Categories:** Travel
- **Stats:** 2 total users, 1 monthly users, 71.4% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 successful flight searches

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### What does Google Flights Scraper & Price Monitor do?

Collect [Google Flights](https://www.google.com/travel/flights) fares for **travel apps and recurring price comparisons**. This Google Flights scraper returns one-way and paired round-trip itineraries, total-party fares, physical flight segments and Google booking links. Search a single route or batch up to **100 route-and-date queries**, then use JSON/CSV exports or the Apify API in your application. It is an independent Google Flights API alternative.

Coverage is limited to observed options, rather than exhaustive flight inventory. For flight price monitoring, schedule runs through Apify, save each run's results and compare them yourself using stable itinerary IDs. The Python tutorial below shows that workflow.

### Why use it for recurring fare comparisons?

- **Join observations across runs.** `itineraryId` excludes the current price, so you can compare the same itinerary over time while keeping the search settings fixed.
- **Use paired round-trip totals.** Each round-trip row includes selected outbound and inbound flights with Google's total fare for the passenger party.
- **Keep currency comparisons consistent.** Each exported fare's source currency is checked against the requested currency.
- **Collect multiple routes in one run.** Batch up to 100 searches, with labels and per-search overrides, for route analysis or an application's data pipeline.
- **Understand collection outcomes.** `RUN_SUMMARY` records per-search status, partial failures and coverage limits so missing results are not mistaken for missing flights.

### Quick start: collect Google Flights prices

1. Open the [Input tab](https://apify.com/vincesoft/google-flights-price-monitor/input-schema), select JSON input and paste this one-way example.
2. Optionally add a future `departureDate` in `YYYY-MM-DD` format. For this one-off quick start, omitting it searches 30 days after the run date in UTC.
3. Set your maximum cost per run to at least **$0.00505**, then start the Actor.
4. Inspect the flight dataset, download its JSON or CSV export, and open the linked run summary to review coverage and status.

```json
{
  "origin": "JFK",
  "destination": "LHR",
  "currency": "USD",
  "maxResultsPerSearch": 10,
  "maxRequestsPerRun": 3
}
```

For repeated observations of one journey, use fixed travel dates as shown below. The date default moves forward each day.

### What data does it return?

| Field | Type | Meaning |
| --- | --- | --- |
| `searchId` | string | Stable key for normalized search settings, including route, dates, filters and passengers |
| `itineraryId` | string | Stable itinerary comparison key that excludes the current price |
| `observedAt` | string | UTC observation start; options are fetched over the following seconds |
| `price`, `currency`, `priceBasis` | number, strings | Source-verified total-party fare, currency and one-way/round-trip basis |
| `outbound`, `inbound` | arrays | Physical segments with airports, airlines, flight numbers and local times |
| `googleBookingUrl` | string | Google itinerary link encoding the selected flights |

The abbreviated round-trip record below is a **historical observation from 8 October 2026**, for travel in November 2026. Its $692 fare is historical test data, not a live offer. Both directions are included in that one total; do not add another outbound price.

```json
{
  "searchId": "734e74e97d3dc2d3397e6cbd",
  "itineraryId": "3ddbaad329a3b242c7fae40c",
  "observedAt": "2026-10-08T13:59:25.710+00:00",
  "tripType": "round-trip",
  "origin": "JFK",
  "destination": "LHR",
  "departureDate": "2026-11-10",
  "returnDate": "2026-11-17",
  "price": 692,
  "currency": "USD",
  "priceBasis": "total-party-round-trip",
  "outbound": [
    {
      "origin": "JFK",
      "destination": "DUB",
      "departureTime": "2026-11-10T17:00",
      "arrivalTime": "2026-11-11T04:30",
      "airlineCode": "EI",
      "flightNumber": "104"
    },
    {
      "origin": "DUB",
      "destination": "LHR",
      "departureTime": "2026-11-11T06:35",
      "arrivalTime": "2026-11-11T08:05",
      "airlineCode": "EI",
      "flightNumber": "152"
    }
  ],
  "inbound": [
    {
      "origin": "LHR",
      "destination": "ZRH",
      "departureTime": "2026-11-17T06:00",
      "arrivalTime": "2026-11-17T08:40",
      "airlineCode": "LX",
      "flightNumber": "345"
    },
    {
      "origin": "ZRH",
      "destination": "JFK",
      "departureTime": "2026-11-17T09:55",
      "arrivalTime": "2026-11-17T13:15",
      "airlineCode": "LX",
      "flightNumber": "16"
    }
  ],
  "googleBookingUrl": "https://www.google.com/travel/flights/booking?tfs=CBwQAhpgEgoyMDI2LTExLTEwIh8KA0pGSxIKMjAyNi0xMS0xMBoDRFVCKgJFSTIDMTA0Ih8KA0RVQhIKMjAyNi0xMS0xMRoDTEhSKgJFSTIDMTUyagcIARIDSkZLcgcIARIDTEhSGl8SCjIwMjYtMTEtMTciHwoDTEhSEgoyMDI2LTExLTE3GgNaUkgqAkxYMgMzNDUiHgoDWlJIEgoyMDI2LTExLTE3GgNKRksqAkxYMgIxNmoHCAESA0xIUnIHCAESA0pGS0ABSAFwAYIBCwj___________8BmAEB&hl=en&gl=US&curr=USD"
}
```

### How to monitor Google Flights prices with Python

#### Save two observations of the same journey

1. Choose a future departure date and save this input as a task in your own Apify account. This example uses **15 January 2027**; update it before that date passes. Add a fixed `returnDate` for a round trip.
2. Run the task. Download the **full dataset as JSON** as `before.json`, including `searchId`, `itineraryId`, `observedAt`, `price`, `currency` and `priceBasis`. Export all rows and fields, without a table-view projection, row limit or field filter. Save that run's key-value record `RUN_SUMMARY` as `before-summary.json`.
3. Run the identical input later, manually or with an Apify schedule. Save the second dataset as `after.json` and its own `RUN_SUMMARY` as `after-summary.json`.
4. Save the complete program below as `compare_fares.py` beside those four files and execute the command beneath it. Python 3.11 or later is sufficient; no packages are required.

```json
{
  "origin": "JFK",
  "destination": "LHR",
  "departureDate": "2027-01-15",
  "currency": "USD",
  "country": "US",
  "cabin": "economy",
  "adults": 1,
  "maxResultsPerSearch": 10,
  "maxRequestsPerRun": 3
}
```

Keep the same route, travel dates, cabin, passengers, currency, country edition, language, filters and sorting. `searchId` captures those normalized settings; `label` and operational limits are excluded. Keep the collection limits consistent too, since they affect coverage. Omitting the departure date would select UTC today plus 30 days, comparing different journeys on different days.

**Missing results are not proof of unavailability.** Even `complete` searches have bounded coverage. A changed departure time creates a new itinerary ID and appears as unmatched observations. Use `itineraryId` to join runs; `observationId` changes with observation time.

#### Copyable comparison program

This local program reads one search's JSON exports, verifies matching searches and chronological observations, and preserves coverage warnings. It rejects `error` and `skipped` searches. A batch must first be reduced to the selected search entry and only that search's rows in both exports. It does not start runs or send notifications.

```python
"""Compare two single-search JSON exports locally using Python 3.11+ only."""
import argparse
import json
import re
import sys
from datetime import datetime
from decimal import Decimal
from pathlib import Path


def required_text(value, key):
    text = value.get(key)
    if not isinstance(text, str) or not text.strip():
        raise ValueError(f'Missing or invalid {key}')
    return text


def timestamp(value):
    if not isinstance(value, str):
        raise ValueError('observedAt must be an ISO timestamp with a timezone')
    try:
        parsed = datetime.fromisoformat(value.replace('Z', '+00:00'))
    except ValueError as error:
        raise ValueError('Invalid observedAt timestamp') from error
    if parsed.tzinfo is None:
        raise ValueError('observedAt must include a timezone')
    return parsed


def observation(rows, summary):
    if not isinstance(rows, list) or not isinstance(summary, dict):
        raise ValueError('Dataset must be a JSON array and summary a JSON object')
    searches = summary.get('searches')
    if not isinstance(searches, list) or len(searches) != 1:
        raise ValueError('Select a single search and its rows from each batch summary')
    search = searches[0]
    if not isinstance(search, dict):
        raise ValueError('Each summary search must be an object')
    status = search.get('status')
    if status not in ('complete', 'partial', 'empty'):
        raise ValueError('Cannot compare an error, skipped, or unknown search status')
    search_id = required_text(search, 'searchId')
    observed_at = required_text(search, 'observedAt')
    timestamp(observed_at)
    query = search.get('search')
    if not isinstance(query, dict):
        raise ValueError('Summary must include the normalized search object')
    currency = required_text(query, 'currency')
    if not re.fullmatch(r'[A-Z]{3}', currency):
        raise ValueError('Invalid summary currency')
    basis = 'total-party-round-trip' if query.get('returnDate') else 'total-party-one-way'
    count = search.get('resultCount')
    if type(count) is not int or count != len(rows):
        raise ValueError('Dataset row count must match summary resultCount; export all rows')
    if (status == 'empty') != (count == 0):
        raise ValueError('Summary status and resultCount are inconsistent')
    indexed = {}
    for row in rows:
        if not isinstance(row, dict):
            raise ValueError('Each dataset row must be an object')
        itinerary_id = required_text(row, 'itineraryId')
        if required_text(row, 'searchId') != search_id:
            raise ValueError('Dataset and summary searchId values differ')
        if timestamp(row.get('observedAt')) != timestamp(observed_at):
            raise ValueError('Dataset and summary observedAt values differ')
        if row.get('currency') != currency or row.get('priceBasis') != basis:
            raise ValueError('Dataset currency or priceBasis differs from the search')
        price = row.get('price')
        if isinstance(price, bool) or not isinstance(price, (int, float, Decimal)):
            raise ValueError('Each price must be a finite positive JSON number')
        price = Decimal(str(price))
        if not price.is_finite() or price <= 0:
            raise ValueError('Each price must be a finite positive JSON number')
        if itinerary_id in indexed and indexed[itinerary_id] != row:
            raise ValueError(f'Conflicting duplicate itineraryId: {itinerary_id}')
        indexed[itinerary_id] = row
    warnings = ['Coverage is bounded; missing itineraries do not establish unavailability.']
    if status != 'complete':
        warnings.append(f'Search status is {status}; results may be incomplete or absent.')
    if search.get('outputTruncated'):
        warnings.append('Output was truncated by the configured result limit.')
    if search.get('coverage'):
        warnings.append(str(search['coverage']))
    if search.get('unpricedOptionsSkipped'):
        warnings.append(f"Unpriced options skipped: {search['unpricedOptionsSkipped']}")
    issues = search.get('issues', [])
    if not isinstance(issues, list):
        raise ValueError('Summary issues must be an array')
    for issue in issues:
        warnings.append('Source issue: ' + json.dumps(issue, default=str, sort_keys=True))
    return {
        'searchId': search_id, 'status': status, 'observedAt': observed_at,
        'currency': currency, 'priceBasis': basis, 'warnings': warnings,
    }, indexed


def compare(before, before_summary, after, after_summary):
    previous, old = observation(before, before_summary)
    current, new = observation(after, after_summary)
    if previous['searchId'] != current['searchId']:
        raise ValueError('searchId values differ; repeat the identical normalized search')
    if (previous['currency'], previous['priceBasis']) != (current['currency'], current['priceBasis']):
        raise ValueError('Currency and priceBasis must match between observations')
    if timestamp(current['observedAt']) <= timestamp(previous['observedAt']):
        raise ValueError('The after observation must be later than the before observation')
    changes = []
    for itinerary_id in sorted(old.keys() | new.keys()):
        earlier, later = old.get(itinerary_id), new.get(itinerary_id)
        old_price = Decimal(str(earlier['price'])) if earlier else None
        new_price = Decimal(str(later['price'])) if later else None
        difference = new_price - old_price if earlier and later else None
        if earlier is None:
            status = 'newly_observed'
        elif later is None:
            status = 'not_observed_later'
        else:
            status = 'decreased' if difference < 0 else 'increased' if difference > 0 else 'unchanged'
        changes.append({
            'itineraryId': itinerary_id, 'status': status,
            'previousPrice': format(old_price, 'f') if old_price is not None else None,
            'currentPrice': format(new_price, 'f') if new_price is not None else None,
            'difference': format(difference, 'f') if difference is not None else None,
            'currency': current['currency'], 'priceBasis': current['priceBasis'],
            'previousObservedAt': earlier['observedAt'] if earlier else None,
            'currentObservedAt': later['observedAt'] if later else None,
        })
    return {'searchId': current['searchId'], 'before': previous, 'after': current, 'changes': changes}


def reject_constant(value):
    raise ValueError(f'Non-finite JSON number: {value}')


def read_json(path):
    return json.loads(Path(path).read_text(encoding='utf-8-sig'),
                      parse_float=Decimal, parse_constant=reject_constant)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    for name in ('before', 'before-summary', 'after', 'after-summary'):
        parser.add_argument('--' + name, required=True)
    args = parser.parse_args()
    try:
        report = compare(read_json(args.before), read_json(args.before_summary),
                         read_json(args.after), read_json(args.after_summary))
    except (OSError, UnicodeError, ValueError) as error:
        print(f'Cannot compare fares: {error}', file=sys.stderr)
        return 1
    print(json.dumps(report, indent=2))
    return 0


if __name__ == '__main__':
    sys.exit(main())
```

Run this as one command on Windows, macOS or Linux:

```text
python compare_fares.py --before before.json --before-summary before-summary.json --after after.json --after-summary after-summary.json
```

The program writes its JSON report to stdout and validation errors to stderr with a nonzero exit code. Monetary values and differences are **decimal strings** to preserve exact arithmetic. `difference` is current minus previous fare in currency units; it is null for unmatched observations. Each entry includes the observation timestamps where present. Collection statuses and warnings are retained under `before` and `after`.

#### Try a constructed example offline

The following fixture generator is **illustrative**, with abbreviated rows and demonstration IDs. It does not represent historical fares or a Google search. Save it as `create_fixtures.py` beside `compare_fares.py`, then run `python create_fixtures.py`. It creates the four JSON files used by the command above in the same folder. Use a separate folder to avoid overwriting your own exports.

```python
"""Write constructed comparison fixtures; these are not historical fare offers."""
import json
from pathlib import Path


def create(directory):
    directory = Path(directory)
    directory.mkdir(parents=True, exist_ok=True)
    query = {'origin': 'JFK', 'destination': 'LHR', 'departureDate': '2027-01-15',
             'currency': 'USD', 'country': 'US', 'cabin': 'economy', 'adults': 1}
    for name, observed_at, fares in [
        ('before', '2026-10-17T10:00:00+00:00', {'demo-a': 320.25, 'demo-b': 450, 'demo-c': 510}),
        ('after', '2026-10-18T10:00:00+00:00', {'demo-a': 299.99, 'demo-b': 450, 'demo-d': 475}),
    ]:
        rows = [{'searchId': 'demo-search', 'itineraryId': key, 'observedAt': observed_at,
                 'price': price, 'currency': 'USD', 'priceBasis': 'total-party-one-way'}
                for key, price in fares.items()]
        summary = {'searches': [{'searchId': 'demo-search', 'search': query, 'status': 'complete',
                    'observedAt': observed_at, 'resultCount': len(rows), 'coverageLimited': True,
                    'coverage': 'Constructed fixture with bounded illustrative coverage.',
                    'outputTruncated': False, 'issues': [], 'unpricedOptionsSkipped': 0}]}
        for filename, value in [(f'{name}.json', rows), (f'{name}-summary.json', summary)]:
            (directory / filename).write_text(json.dumps(value, indent=2) + '\n', encoding='utf-8')


if __name__ == '__main__':
    create(Path(__file__).resolve().parent)
```

The constructed report contains these four observations:

| Itinerary | Classification | Previous USD | Current USD | Difference USD |
| --- | --- | --- | --- | --- |
| `demo-a` | `decreased` | 320.25 | 299.99 | -20.26 |
| `demo-b` | `unchanged` | 450 | 450 | 0 |
| `demo-c` | `not_observed_later` | 510 | null | null |
| `demo-d` | `newly_observed` | null | 475 | null |

For example, the first entry in the report's `changes` array is:

```json
{
  "itineraryId": "demo-a",
  "status": "decreased",
  "previousPrice": "320.25",
  "currentPrice": "299.99",
  "difference": "-20.26",
  "currency": "USD",
  "priceBasis": "total-party-one-way",
  "previousObservedAt": "2026-10-17T10:00:00+00:00",
  "currentObservedAt": "2026-10-18T10:00:00+00:00"
}
```

`partial` and `empty` searches can be compared, but carry explicit warnings. Missing rows remain observations about collection, not conclusions about cancellations, sold-out flights or price increases.

### Batch searches and public example tasks

Use these two configurations to save tasks in your own account. Update their travel dates before running. Each example is limited to 10 results per search, disables optional proxies, and allows one retry. A **$0.02 maximum cost per run** covers their configured Actor charges. Copied tasks keep your own dates and settings; schedules must be configured separately.

#### Compare Google Flights prices across multiple routes

Try the [public route-comparison example](https://apify.com/vincesoft/google-flights-price-monitor/examples/compare-google-flights-routes), or copy this input into your own task.

Collect JFK–London and JFK–Paris one-way fares for the same day, in USD. Export the observations for route analysis or an application; the Actor does not automatically produce a route-ranking report.

```json
{
  "currency": "USD",
  "country": "US",
  "language": "en",
  "cabin": "economy",
  "adults": 1,
  "maxResultsPerSearch": 10,
  "maxRetries": 1,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "searches": [
    {
      "label": "London",
      "origin": "JFK",
      "destination": "LHR",
      "departureDate": "2027-01-15"
    },
    {
      "label": "Paris",
      "origin": "JFK",
      "destination": "CDG",
      "departureDate": "2027-01-15"
    }
  ],
  "maxRequestsPerRun": 10
}
```

Use `searches` **or** the single-search route/date fields, never both. Each batch entry requires `origin`, `destination` and `departureDate`; it may override global settings and carry a `label`. Equivalent duplicate searches run once, using the first label.

#### Collect round-trip Google Flights fares and itineraries

Try the [public round-trip example](https://apify.com/vincesoft/google-flights-price-monitor/examples/collect-google-flights-round-trip-fares), or copy this input into your own task.

Collect JFK–London options for a seven-day journey. Each row includes paired directions and one total-party fare. The configuration expands two outbound options, so round-trip coverage is bounded by that choice. Google can return an internal error for an expanded outbound. In that case useful pairs are retained with a `partial` summary; inspect that summary before comparing observations. If no usable options can be collected, the run reports an error instead of treating the response as no availability.

```json
{
  "currency": "USD",
  "country": "US",
  "language": "en",
  "cabin": "economy",
  "adults": 1,
  "maxResultsPerSearch": 10,
  "maxRetries": 1,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "origin": "JFK",
  "destination": "LHR",
  "departureDate": "2027-01-15",
  "returnDate": "2027-01-22",
  "maxOutboundOptions": 2,
  "maxRequestsPerRun": 6
}
```

### API access and JSON/CSV exports

Use the [API tab](https://apify.com/vincesoft/google-flights-price-monitor/api/python) to generate authenticated requests and the [Input tab](https://apify.com/vincesoft/google-flights-price-monitor/input-schema) for the complete configuration. The Actor identifier is `vincesoft/google-flights-price-monitor`. Pass the same JSON input and set the maximum charge limit in your API run options. Apify's MCP tools can also invoke the Actor with this input.

You can download the dataset in JSON, HTML, CSV or Excel. Each row is one observed itinerary option; the separate `RUN_SUMMARY` key-value record explains collection outcomes. Retain both for recurring comparisons. The default table shows 10 useful columns, while the full dataset has 28 top-level fields. The comparison program requires the full JSON export, not a download restricted to the default table columns.

### Pricing

**$0.005 per successful search ($5/1,000 searches)**, charged once per unique query that delivers at least one validated option. A partial search with useful results is charged once. Failed, empty and skipped searches receive no search charge. Apify also charges **$0.00005 per run** for the Actor start event, even when no search returns results. There is no per-itinerary dataset charge.

One successful search costs **$0.00505** in Actor charges. Two successful searches in one run cost **$0.01005**. The minimum allowed charge limit is **$0.00505**. If the remaining budget cannot cover another batch search, it is skipped and recorded in `RUN_SUMMARY`.

One-way searches normally require one Google request. A round trip requires an outbound request plus one return-selection request per expanded outbound, before retries. `maxRequestsPerRun` caps Google requests across the entire run; it is an operational limit, separate from the monetary spending limit. Optional proxies can add platform costs and are disabled by default. The Actor runs at 256 MB and requires no paid upstream search API.

### Coverage, data quality, and troubleshooting

The Actor reads observed options in Google's initial page data. It does not claim exhaustive inventory. Round trips expand only the configured number of outbound options; their prices come from Google's return-selection page and are never the sum of an outbound estimate and a round-trip total.

Currency is read from the source fare token and checked against the requested currency. Unpriced options are skipped and counted in `unpricedOptionsSkipped`. Google can withhold inline fares for children or infants, reducing coverage. Missing optional fields remain null. Segment timestamps use each airport's local clock without an inferred UTC offset. `requestedCountry` is the requested Google edition, not independent proof of the proxy's point of sale.

`maxStops` is -1 for unrestricted, 0 for nonstop, 1 for up to one connection, and 2 for up to two per direction. Optional `airlines` filters are sent to Google; codeshares can involve other operators. `maxPrice` limits the total party fare. `sortBy` supports `price`, `duration`, `departure` and `google`. Passenger counts are limited to nine total, with no more lap infants than adults.

#### What do the search statuses mean?

| Status | Meaning |
| --- | --- |
| `complete` | The configured observed-options workload finished without detected extraction issues; coverage is still limited. |
| `partial` | Usable options were saved with collection/validation issues or an explicit passenger-coverage warning. |
| `empty` | No matching validated options were observed; this does not prove no flights operate. |
| `error` | Extraction or return expansion failed without a usable option. |
| `skipped` | A stop or spending limit prevented starting the search. |

The summary includes validation issues, output truncation, outbound expansion counts, request counts, response bytes and duration. If every search errors without usable options, the Actor fails after saving the summary. A mixed batch preserves useful results.

#### What if Google blocks a request or changes its page structure?

Retries stay within configured limits. Unrecognized pages are recorded as errors or partial results, rather than proof of no availability. Review `RUN_SUMMARY` first. Stop/resurrection handling saves progress at completed-search boundaries; storage writes and charges are not a single transaction, so abrupt process failure can require reconciling output and charges.

### FAQ and support

#### Is this an official Google Flights API?

No. It is independent of Google and extracts public flight-search information. Use it according to applicable website terms and data-use requirements. It does not collect account access, personal traveler data or profiles.

#### Are price alerts and history built in?

The Actor collects current observations. Use Apify schedules to repeat a task, retain each run's dataset and summary, and compare the saved results using the tutorial. Your application supplies durable history, alerts and notifications.

#### How are round-trip prices represented?

Each paired round-trip row contains outbound and inbound physical flight segments and a total fare for the requested passenger party. `priceBasis` is `total-party-round-trip`. A one-way row uses `total-party-one-way` and an empty inbound array.

#### Does the booking link reserve seats or guarantee the fare?

No. It opens a selected Google itinerary. Prices and availability can change before reaching the supplier. The Actor does not resolve individual airline/OTA offers or complete bookings.

#### How do I report an issue?

Submit reproducible input, the error code and relevant summary details through the [Issues tab](https://apify.com/vincesoft/google-flights-price-monitor/issues/open). Omit credentials. Use the [API tab](https://apify.com/vincesoft/google-flights-price-monitor/api/python) for programmatic invocation examples.

# Actor input Schema

## `origin` (type: `string`):

Three-letter departure airport IATA code, such as JFK.

## `destination` (type: `string`):

Three-letter arrival airport IATA code, such as LHR.

## `departureDate` (type: `string`):

Future outbound date in YYYY-MM-DD. For a single search, omission selects 30 days after the run date (UTC).

## `returnDate` (type: `string`):

Optional return date for a paired round trip. Omit for a one-way search.

## `currency` (type: `string`):

Requested ISO 4217 currency. Each returned fare must independently match it.

## `country` (type: `string`):

Google country edition (gl), not a guarantee of proxy IP or point of sale.

## `language` (type: `string`):

Google interface language, such as en or en-GB.

## `cabin` (type: `string`):

Requested seating class.

## `adults` (type: `integer`):

Adult passengers. All passenger types combined are limited to nine.

## `children` (type: `integer`):

Children aged 2–11. Google may inline fewer priced results for children.

## `infantsInSeat` (type: `integer`):

Infants under two in their own seat. Coverage may be reduced.

## `infantsOnLap` (type: `integer`):

Infants on an adult lap; cannot exceed the adult count.

## `maxStops` (type: `integer`):

-1 means unrestricted, 0 nonstop, 1 up to one stop, 2 up to two stops, each direction.

## `airlines` (type: `array`):

Optional included airline codes. Sent to Google; codeshares may involve other operators.

## `maxPrice` (type: `number`):

Optional maximum total fare for the entire passenger party, in the requested currency.

## `sortBy` (type: `string`):

Ordering applied to the observed options. Google preserves its source grouping.

## `searches` (type: `array`):

Use a batch OR the single route/date fields above. Each entry can override global search defaults.

## `maxResultsPerSearch` (type: `integer`):

Maximum dataset rows saved for each search.

## `maxOutboundOptions` (type: `integer`):

Expand this many outbound choices into real return options. A round trip costs 1 + this many successful page requests, before retries.

## `maxRequestsPerRun` (type: `integer`):

Hard cap on all Google requests, including retries and return expansion.

## `maxRetries` (type: `integer`):

Additional attempts after an unsuccessful Google request.

## `requestTimeoutSeconds` (type: `integer`):

Maximum seconds per Google request.

## `minRequestIntervalMillis` (type: `integer`):

Minimum milliseconds between starting Google requests across the entire run.

## `maxConcurrency` (type: `integer`):

Maximum concurrent independent searches. Request interval and total request cap still apply.

## `proxyConfiguration` (type: `object`):

Optional Apify or custom proxies. Disabled by default; enabling Apify proxies may add platform costs.

## Actor input object example

```json
{
  "origin": "JFK",
  "destination": "LHR",
  "currency": "USD",
  "country": "US",
  "language": "en",
  "cabin": "economy",
  "adults": 1,
  "children": 0,
  "infantsInSeat": 0,
  "infantsOnLap": 0,
  "maxStops": -1,
  "airlines": [],
  "sortBy": "price",
  "maxResultsPerSearch": 50,
  "maxOutboundOptions": 3,
  "maxRequestsPerRun": 100,
  "maxRetries": 2,
  "requestTimeoutSeconds": 30,
  "minRequestIntervalMillis": 1000,
  "maxConcurrency": 2,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `flights` (type: `string`):

Dataset of observed flight options.

## `summary` (type: `string`):

Per-search coverage and status.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "origin": "JFK",
    "destination": "LHR"
};

// Run the Actor and wait for it to finish
const run = await client.actor("vincesoft/google-flights-price-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "origin": "JFK",
    "destination": "LHR",
}

# Run the Actor and wait for it to finish
run = client.actor("vincesoft/google-flights-price-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "origin": "JFK",
  "destination": "LHR"
}' |
apify call vincesoft/google-flights-price-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,vincesoft/google-flights-price-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/FHiw30C01bbfhNykM/builds/5tmZqmwiSDQXsfgOX/openapi.json
