# Google Flights Price Fetcher - route & date search (`t2solutions/google-flights-price-fetcher`) Actor

Fetch real Google Flights offers as clean JSON: carrier, times, stops, segments, price. Plain HTTP, no browser needed. Batch several routes in one run. Every run ends with a RUN_SUMMARY row saying exactly what happened: complete, timed out, rate limited, or zero match. Priced per flight row.

- **URL**: https://apify.com/t2solutions/google-flights-price-fetcher.md
- **Developed by:** [T2 Solutions](https://apify.com/t2solutions) (community)
- **Categories:** Travel, Automation, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.20 / 1,000 results

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

## Google Flights Price Fetcher — route & date search

Fetch real flight offers from **Google Flights** for an origin → destination, one day or a
round trip: carrier, flight times, aircraft, stops, segment detail and the **price Google
shows** — as clean JSON, one row per offer, ready for Sheets, databases or an agent pipeline.

No headless browser. No login. No proxies. One paced plain-HTTP request per search, exactly
like a human tab — because that is what works: Google serves the full offer data
server-rendered in the results page, and this actor parses it with a fixture-pinned,
test-verified layout contract.

### Why use this actor

- 🎯 **The price Google shows** — the number a shopper sees, not an inferred estimate. Plus each offer's segment list (flight numbers, aircraft) when it's a multi-leg trip.
- ⚡ **Plain HTTP, no browser spin-up** — most rivals run Puppeteer/Playwright; that burns compute and adds flakiness. This one reads the page like the Transport you already have: one GET per search.
- 🧾 **It reports its own honesty** — every run ends with exactly one `RUN_SUMMARY` row (`complete` / `partial` / `timed_out` / `zero_match`), so an empty result set never masquerades as a success.
- 🧮 **Batch input** — search several routes in one run with a shared fail-safe deadline.
- 🤖 **Agent-ready** — deterministic JSON (`offerId` fingerprints are stable across runs), `LIMITED_PERMISSIONS`-compatible, and an MCP section below: an agent can drive it unattended.
- 🛡️ **Fail-safe deadline** — never hangs past a wall you set; partial results are flushed and honestly labeled instead of vanishing.

### Input

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `jobs` | array of objects (required) | — | One or more route jobs (see below). |
| `failSafeDeadlineMinutes` | integer | `55` | Hard wall for the whole run. On expiry running stops, fetched rows are kept, and `RUN_SUMMARY` records `timed_out`. A value ≤ 0 fails fast. |
| `pacingSeconds` | integer | `20` | Seconds between search fetches (min 5). Higher is gentler; lower is faster. |
| `maxOffers` | integer | `200` | Flight rows kept per job (Google returns best-first; the cap applies after that order). |

#### Job object (route type)

| Field | Type | Description |
|-------|------|-------------|
| `type` | string | `"route"` (default). A `"calendar"` job is accepted but completes as an explicitly-null calendar entry with a documented reason in `RUN_SUMMARY.calendarUnavailable` (v1 scope: routes). |
| `label` | string | Your name for the job; shows up on every row. Default `job-N`. |
| `origin` | string (required) | Origin airport/city code, e.g. `JFK`. |
| `destination` | string (required) | Destination code, e.g. `LAX`. |
| `departureDate` | string (required) | `YYYY-MM-DD`. |
| `returnDate` | string (optional) | Return date for round trips (`on X through Y`). Omit for one-way. |
| `cabin` | string (optional) | `ECONOMY` (default), `PREMIUM_ECONOMY`, `BUSINESS`, `FIRST`. |
| `passengers` | integer (optional) | 1–9; appended as `N adults` when > 1. |
| `maxOffers` | integer (optional) | Per-job cap, overriding the global `maxOffers` for this job only. |

**Example input** — three routes in one run:

```json
{
  "jobs": [
    { "type": "route", "label": "jfk-lax-rt", "origin": "JFK", "destination": "LAX",
      "departureDate": "2026-11-07", "returnDate": "2026-11-14" },
    { "type": "route", "label": "sfo-nrt-jan", "origin": "SFO", "destination": "NRT",
      "departureDate": "2027-01-10", "returnDate": "2027-01-20", "cabin": "BUSINESS" },
    { "type": "route", "label": "mia-gru-oneway", "origin": "MIA", "destination": "GRU",
      "departureDate": "2026-11-21" }
  ],
  "failSafeDeadlineMinutes": 55
}
```

### Output

One row per flight offer (columns per the table), then **exactly one** final `RUN_SUMMARY` row.

| Column | Nullable | Notes |
|--------|----------|-------|
| `recordType` | no | Absent on offer rows; `"RUN_SUMMARY"` on the final row. |
| `offerId` | no | Stable fingerprint (route + times + carrier + price + flight number) — identical queries across runs give identical IDs, so agents can diff price changes. |
| `jobLabel` | no | The job's label. |
| `origin` / `destination` | no | IATA codes as parsed from the offer. |
| `departureAt` / `arrivalAt` | no | Local (airport-local) time, ISO-like `YYYY-MM-DDTHH:MM:SS`. |
| `durationMinutes` | yes | Computed from the offered times (minutes). |
| `stops` | yes | `0` = nonstop. |
| `segments` | yes | For multi-leg trips: per-segment airports, times, dates, flight number, aircraft. `null` when not published. |
| `carrier` | yes | Display name (e.g. `"Delta"`), else the code. |
| `priceUsd` | no (skipped rows counted) | The price Google shows. |
| `pricePerPassengerUsd` | always null | No per-passenger split is published; we don't invent numbers. |
| `bookingUrl` | always null | Google's booking blob is opaque, not a URL; we don't fabricate links. |
| `fareBrand` | always null | Not exposed by the results page payload. |
| `fetchedAt` | no | UTC timestamp of the fetch. |

#### RUN_SUMMARY (the honesty row)

| Field | Meaning |
|---|---|
| `state` | `complete` · `partial` (some jobs failed or input was dropped) · `timed_out` (deadline wall) · `zero_match` (every job fetched fine but Google returned no offers) |
| `rowsRequested` / `rowsFetched` | Capped demand vs what was delivered. |
| `jobsTotal` / `jobsOk` / `jobsFailed` | Per-job counts. |
| `partialReason` | `deadline` · `rate_limited` · `parse_failure` · `job_cap` · `row_cap` · `input_invalid` · … |
| `calendarUnavailable` | Calendar-type jobs with the documented v1 reason (explicitly-null calendar, never an error loop). |
| `jobResults` | Per-job outcome: rows, skipped/unparsed/duplicate offer counts. |

**Zero-match example** (a real empty run — an agent should read this, not panic):

```json
{
  "recordType": "RUN_SUMMARY",
  "state": "zero_match",
  "rowsRequested": 200,
  "rowsFetched": 0,
  "jobsTotal": 1,
  "jobsOk": 1,
  "jobsFailed": 0,
  "partialReason": null,
  "calendarUnavailable": null,
  "jobResults": [ { "label": "jfk-lax", "status": "ok", "rows": 0 } ]
}
```

Charging matches this row: you pay per flight row (`result` event); a zero-match run charges **zero** results.

### Use with AI agents (MCP)

Every Apify actor is callable by AI agents — this one is built to behave when they drive it:

- **Apify MCP server** — add `https://mcp.apify.com` as an MCP server in your agent (Claude, Cursor, etc.), authenticate with an Apify API token, and this actor appears as a tool whose input is the plain JSON schema above.

- **REST** — one call starts a run, one call reads the results:

  ```bash
  curl -X POST "https://api.apify.com/v2/acts/YOUR~google-flights-price-fetcher/runs?token=YOUR_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"jobs":[{"origin":"JFK","destination":"LAX","departureDate":"2026-11-07"}]}'
  # results: GET /v2/acts/YOUR~google-flights-price-fetcher/runs/{runId}/dataset/items?token=YOUR_TOKEN
  ```

- **Safe to hand over** — the actor runs under Apify's `LIMITED_PERMISSIONS`: a run can only do what a run needs (its own dataset and key-value store, plus the Google Flights page it fetches), nothing else in your account.

- **It reports its own honesty** — the `RUN_SUMMARY` row means an agent can tell a complete scan from a timed-out, rate-limited, invalid-input or zero-match one without guessing from an empty dataset.

- **Batch input** — `"jobs": [ {...}, {...} ]` resolves several routes in one call, which is how agents usually want to use it.

### Migrating from another Google Flights scraper?

An honest comparison — including where another tool may still be right. (Figures are what the respective Store pages showed in our late-October 2026 recon; verify live stats yourself.)

| If you're on | What changes |
|---|---|
| **kaix/google-flights-scraper** (volume leader) | Cheaper per flight ($0.00006) and hugely popular — but no rating and no review presence. This actor is for agents who need the `RUN_SUMMARY` honesty contract and stable `offerId`s more than the absolute floor price. |
| **johnvc/Google-Flights-Data-Scraper** | Priced per *page* ($0.03/page); this one is per flight row, and you can cap rows per job. |
| **memo23/google-flights-scraper** | memo23 is the field's README/quality bar; this actor answers with plain-HTTP speed (no browser compute), per-row pricing, and the agent-facing honesty row. |
| **lergassy/google-flights-scraper** | Strongest rated (4.86★/11); a fair pick if breadth of output fields matters more than pacing control and batch deadlines. |
| **automation-lab/google-flights-scraper** | ~40% of runs were failing in our snapshot — the actor's core problem. Every run here ends in a state that says what actually happened. |

**Not the right tool if:** you need fare-class breakdowns, seat maps, or per-passenger price splits — Google's results-page payload doesn't publish them and we won't invent them.

### Pricing (proposed — pending publish sign-off)

Pay-per-event: you pay per flight row delivered, and a zero-match run charges zero results.
Two events total: **`result`** per flight row (above), plus the platform-standard
**`apify-actor-start`** at **$0.00005 per run** — it covers the first 5 seconds of compute
and is charged by Apify automatically, independent of results.

| | Price per flight row |
|---|---|
| **This actor (proposed)** | **$0.0002** |
| Field floor (kaix) | $0.00006 |
| Field median | ~$0.002 |
| Per-page rivals | $0.03/page |

Why $0.0002: at ~1–30 offers per typical route query, a single-route run lands around **$0.0004 – $0.006** — below the field median and well above the floor. Platform usage (compute) is billed separately under Apify's usage pricing (runs are recorded with usage billing model `USER`), so the per-row price is pure event value for clean, agent-friendly data. Final number is decided at publish time (see the control-plane `MONEY.md` margin check).

### FAQ

**How fresh are the prices?** As fresh as a search: one paced GET per job at run time. Prices are Google's own snapshot from your run's moment, not a cached feed.

**Does this hit rate limits?** Pacing defaults to 20 s between searches with 30/120/300 s backoff on a rate limit — the same posture Google demonstrably tolerates for normal users (verified with a 20-request sustained probe over 8 minutes: 100% clean). A rate-limited job is retried, then recorded as `partialReason: rate_limited`, never silently dropped.

**Round trips?** Yes — set `returnDate`. The search covers the pair in one query, exactly as the site does.

**Can I get the price calendar (cheapest by date)?** Not yet. Calendar-type jobs are accepted and complete with an explicitly-null calendar plus a documented reason in `RUN_SUMMARY.calendarUnavailable` — it's the honest "not built yet", not an error.

**Can I localize?** The query pins `hl=en&curr=USD` (defaults); cabin and passengers are honored. Non-USD/localized output is on the roadmap.

**Something failed mid-run.** Read `RUN_SUMMARY`: `jobsFailed` and per-entry `reason` tell you which route and why. Partial runs keep the rows that succeeded.

### Development (this repo)

```bash
python test_fetcher.py       # 39 tests — fixture-pinned layout contract + transport
python test_main_wiring.py   # 26 tests — run orchestration with a fake SDK
python -m main               # local run with INPUT.json in cwd
```

Design/parity docs: `_bmad-output/architecture.md` (endpoint choice, failure-mode table, charging, caps), `_bmad-output/fixture-flights-jfk-lax.html` (golden page the parse paths are pinned to), spec in `_bmad-output/specs/spec-google-flights-scraper/`.

Ops: run-outcome alerts go to Telegram — four Apify webhooks, one per terminal status (✅ ❌ ⛔ ⏱️). IDs, payload format, testing and token handling: `WEBHOOKS.md`.

### Related actors

Same developer, adjacent surfaces — all three share the `RUN_SUMMARY` completeness contract, `LIMITED_PERMISSIONS`, and pay-per-result pricing, so an agent that drives one can drive the others without new glue:

- **[Google Ads Transparency Scraper](https://apify.com/t2solutions/google-ads-transparency-scraper)** — the ad-intel cluster's hero: search competitor ads by keyword, domain or advertiser id, with real landing pages (OCR included) across ~46 markets.
- **[TikTok Creative Center Ads Fetch](https://apify.com/t2solutions/tiktok-creative-center-ads-fetch)** — top TikTok ads by market & period.

If you're pricing a trip *and* watching what a travel brand advertises, the two compose: flights for the offer, transparency for the campaign.

# Actor input Schema

## `jobs` (type: `array`):

One or more flight-search jobs. Each job is a route query (origin → destination, dates). Calendar-type jobs are accepted but, in v1, complete as explicitly-null calendar entries with a documented reason in RUN_SUMMARY.calendarUnavailable.

## `failSafeDeadlineMinutes` (type: `integer`):

Hard wall for the whole run. When it is reached, running stops, rows fetched so far are kept, and RUN_SUMMARY records state=timed_out. Never silently truncates. Default 55 (below the platform timeout).

## `pacingSeconds` (type: `integer`):

Seconds between result-page fetches. Default 20 (minimum 5). Higher = gentler on Google, slower run.

## `maxOffers` (type: `integer`):

Flight rows kept per job (after Google's own ordering — best flights first). Default 200, max 200.

## Actor input object example

```json
{
  "jobs": [
    {
      "type": "route",
      "label": "jfk-lax-rt",
      "origin": "JFK",
      "destination": "LAX",
      "departureDate": "2026-11-07",
      "returnDate": "2026-11-14"
    }
  ],
  "failSafeDeadlineMinutes": 55,
  "pacingSeconds": 20,
  "maxOffers": 200
}
```

# Actor output Schema

## `offers` (type: `string`):

Dataset of flight offers (one row per offer: route, times, carrier, price, segments) plus a final RUN_SUMMARY record.

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

RUN_SUMMARY stored in the key-value store under key OUTPUT (state, row/job counts, partial reason).

# 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 = {
    "jobs": [
        {
            "type": "route",
            "label": "jfk-lax-rt",
            "origin": "JFK",
            "destination": "LAX",
            "departureDate": "2026-11-07",
            "returnDate": "2026-11-14"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("t2solutions/google-flights-price-fetcher").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 = { "jobs": [{
            "type": "route",
            "label": "jfk-lax-rt",
            "origin": "JFK",
            "destination": "LAX",
            "departureDate": "2026-11-07",
            "returnDate": "2026-11-14",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("t2solutions/google-flights-price-fetcher").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 '{
  "jobs": [
    {
      "type": "route",
      "label": "jfk-lax-rt",
      "origin": "JFK",
      "destination": "LAX",
      "departureDate": "2026-11-07",
      "returnDate": "2026-11-14"
    }
  ]
}' |
apify call t2solutions/google-flights-price-fetcher --silent --output-dataset

```

## MCP server setup

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

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/oPYecuyv1IxgrUPJ7/builds/t7qpfad035uiQrdpn/openapi.json
