npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

google-travel-website-api

v0.5.0

Published

Google Travel hotel, flight, and vacation rental operations powered by website-api

Downloads

2,275

Readme

Google Travel website-api integration

Application code imports the public package name, never its internal source paths. Network inventory, batchexecute decoding/grouping, catalog comparison, date and JSON helpers, option validation, and filter normalization all come from that shared API — as does the suite model itself: every operation below is a defineOperation the framework selects, documents, and validates:

import { executeOperation } from "google-travel-website-api";

const hotels = await executeOperation({
  operation: "search",
  query: "Boston hotels",
  sort: "highest-rating",
  guestRating: 4.5,
  hotelClass: [4, 5],
  limit: 10,
});

const candidateResult = await executeOperation({
  operation: "hotel-candidates",
  query: "Boston Harbor Hotel",
});
// Inspect candidateResult.candidates and match both name and address; never select the first row blindly.
const selectedEntityToken = "ENTITY_TOKEN_FROM_MATCHED_PROPERTY";

const calendar = await executeOperation({
  operation: "hotel-calendar",
  entityId: selectedEntityToken,
  calendarStart: "2026-08-10",
  calendarEnd: "2026-09-30",
  nights: 1,
  currency: "USD",
});

Install, build, and run:

pnpm install --filter google-travel-website-api
pnpm build
pnpm start --operation catalog
pnpm start --operation hotel-json --query "Boston hotels" --limit 10
pnpm start --operation hotel-candidates --query "Boston Harbor Hotel"
pnpm start --operation hotel-filter-options --query "Boston hotels"
pnpm start --operation hotel-filters --query "Boston hotels" --sort highest-rating --guest-rating 4.5 --hotel-class 4,5
pnpm start --operation autocomplete --query "Boston Harbor Hotel"
pnpm start --operation hotel-map --query "Boston hotels" --limit 100
pnpm start --operation hotel-calendar --entity-id "ENTITY_TOKEN_FROM_MATCHED_PROPERTY" --calendar-start 2026-08-10 --calendar-end 2026-09-30 --nights 1 --currency USD
pnpm start --operation detail --entity-id "ENTITY_TOKEN_FROM_MATCHED_PROPERTY"
pnpm start --operation hotel-all --url "https://www.google.com/travel/search?ts=..."
pnpm start --operation hotel-payloads --query "Boston hotels" --save-raw --out-dir ./captures
pnpm start --operation vacation-rental-catalog
pnpm start --operation vacation-rental-filter-options --query "Boston vacation rentals"
pnpm start --operation vacation-rental-search --query "Boston vacation rentals" --rental-property-types apartments,houses --bedrooms 2 --bathrooms 1 --amenities kitchen,pool
pnpm start --operation vacation-rental-all --url "https://www.google.com/travel/hotels/entity/..."
pnpm start --operation vacation-rental-payloads --query "Boston vacation rentals" --save-raw --out-dir ./captures
pnpm start --operation scan --query "Boston hotels"
pnpm start --operation flight-catalog
pnpm start --operation flight-filter-options
pnpm start --operation flight-autocomplete --query NYC
pnpm start --operation flight-search --origin JFK --destination LAX --departure 2026-08-20 --return 2026-08-27 --limit 10
pnpm start --operation flight-search --origin JFK --destination LAX --departure 2026-08-20 --trip-type one-way --cabin-class business --passengers 2 --max-stops non-stop --departure-window 6-20 --airlines DL --sort cheapest --currency USD
pnpm start --operation flight-search --origin JFK --destination LAX --departure 2026-08-20 --return 2026-08-27 --select 1
pnpm start --operation flight-dates --origin FOC --destination /m/02_286 --departure 2026-10-15 --calendar-start 2026-10-01 --calendar-end 2026-10-31 --trip-type one-way
pnpm start --operation flight-booking --origin JFK --destination LAX --departure 2026-08-20 --select 1
pnpm start --operation flight-booking --origin JFK --destination LAX --departure 2026-08-20 --return 2026-08-27 --select 1 --select-return 2
pnpm start --operation flight-scan --origin JFK --destination LAX --departure 2026-08-20 --return 2026-08-27 --endpoints-only

Selecting an operation

Every operation is declared with its own inputs, and the framework does the selecting. Name it as the first argument or with --operation; both spellings work on the CLI, through runSite, and as POST /v1/sites/google-travel/<operation>:

npx website-api google-travel hotel-candidates --query "Boston Harbor Hotel"
npx website-api google-travel --operation hotel-candidates --query "Boston Harbor Hotel"

There is no default operation: running the site without naming one lists every operation rather than quietly running the catalog. Help is per operation, because the flags are:

npx website-api google-travel --help                # every operation, grouped by surface
npx website-api google-travel flight-dates --help   # only this operation's inputs

A flag belonging to a different operation now says so — Option --hotel-class belongs to "hotel-filter-options"; this is "flight-search" — instead of being accepted and ignored. Two defaults follow the operation that owns them rather than the whole site: --nights is 1 on hotel-calendar and 7 on flight-dates, and --select is 0 on flight-search (return the outbound list) but 1 on flight-booking, which requires a selection.

For the published extension, register and install it before the first operation in a fresh environment. Registry addition is idempotent; --refresh avoids retaining a stale registry or installed bundle:

npx website-api ext registry add https://unpkg.com/google-travel-website-api@latest/registry
npx website-api ext install google-travel --refresh -y
npx website-api ext list
npx website-api google-travel --operation hotel-json --query "Kyoto hotels"

Do not call ext run before this bootstrap: an unconfigured registry reports Site "google-travel" not found in any registry. For one-shot execution after bootstrap, put the trust option before the extension id. ext run passes arguments after the id through to the extension, so ext run google-travel -y does not set the runner's trust option:

npx website-api ext run -y google-travel -- --operation hotel-json --query "Kyoto hotels"

Tests:

pnpm test

# Opt-in live HTTP matrix (Hotels + Flights)
GOOGLE_TRAVEL_LIVE=1 node --test --test-concurrency=1 dist/test/live.test.js

# Rerun a single live case
GOOGLE_TRAVEL_LIVE=1 \
GOOGLE_TRAVEL_LIVE_SCOPE="complete HTTP flight scan" \
node --test --test-concurrency=1 dist/test/live.test.js

Release from a clean working tree with an exact version:

pnpm release 0.3.0

The release command verifies npm authentication and version availability, builds and tests the package, commits the generated registry, creates the version tag, publishes the exact tested files, pushes the branch and tag, and verifies the published npm version.

A response-contract change — a renamed or removed field, or a field that moves behind --raw — breaks existing callers even on 0.x, so release it as a minor bump rather than a patch. The current contract (endpoints replacing dataEndpoints/allEndpoints, url replacing finalUrl, endpointCatalog for published inventory, and --raw gating Google's payloads) is one such change: pnpm release 0.3.0.

Module layout

src/
  index.ts               public API surface
  cli.ts                 CLI entry
  execute-operation.ts   programmatic entry
  extension-site.ts      registry/extension wrapper
  google-travel.ts       the two site contracts
  parameters.ts          the shared input vocabulary, as defineParameters groups

  core/                  primitives with no website-api equivalent
    json-utils.ts  network.ts  protobuf.ts  rate-limit.ts

  google/                Google transport, error envelopes, RPC catalog, shared operation plumbing
    catalog.ts  catalog-operations.ts  envelope.ts  http.ts  runtime.ts

  hotels/                autocomplete, entity tokens, filters, payload decoding, operations
    autocomplete.ts  entity-token.ts  filters.ts  json.ts  markers.ts  operations.ts

  flights/               filters, location entities, RPC codec, deep links, operations
    filters.ts  links.ts  locations.ts  operations.ts  rpc.ts

google-travel.ts holds only what the whole site shares; each operation is a defineOperation beside the code that performs it, in hotels/operations.ts, flights/operations.ts, and google/catalog-operations.ts, over the shared google/runtime.ts. Neither domain imports the other. There is no dispatch table and no published operation list to keep in sync: website-api selects the operation and generates help, the OpenAPI document, and the registry entry from the same declarations.

Date, JSON, batchexecute, network-inventory, catalog, filter, and option helpers come from website-api directly rather than through a local re-export. core/ keeps only what the shared API has no equivalent for: the token-bucket pacing Google's quiet quota requires, the protobuf codec behind tfs and entity tokens, the Google-origin URL default, and the Google-specific endpoint inventory built on the shared primitive.

Response contract

Every operation answers with a JSON object built the same way:

| Field | Present | Meaning | | --- | --- | --- | | operation | always | The operation that produced this response, echoed back. | | endpoints | always | The HTTP requests this run actually made, one entry per method/path/RPC. Empty for an operation that answers from published data. | | url | when applicable | The Google URL the operation worked from or an equivalent public deep link. | | rpcIds, rpcGroups | lodging batchexecute operations | Which multiplexed RPCs answered — with their catalog name, purpose, source paths, and counts. Named flight-service operations report rpc instead. | | count, totalCount | when applicable | The returned row count and, where available, the total matched count. | | endpointCatalog | the four catalog operations | The known endpoint inventory, which is a different record than an observed request. | | google, googleJson, requests | normally --raw only | Google's original positional payloads and f.req envelopes. The two *-payloads operations expose google by default because transport inspection is their purpose. |

endpoints is the single endpoint field — the previous dataEndpoints and allEndpoints are gone — and it always means the same thing: requests this run made, as EndpointHit records with key, method, origin, path, queryKeys, rpcIds, and counts. Published inventory is a different kind of record (EndpointCatalogEntry: id, category, description, implemented) and lives under endpointCatalog, so one field name never carries two shapes. catalog, hotel-catalog, vacation-rental-catalog, flight-catalog, and flight-filter-options issue no request, so their endpoints is [].

The top-level transport views are opt-in for normal search, detail, calendar, autocomplete, scan, and flight operations. They repeat the same data as several positional representations and dominate the response: a three-result hotel-json is ~250 KB by default and ~4.7 MB with --raw. Pass --raw when inspecting a normal operation. Use hotel-payloads / vacation-rental-payloads when the transport is the subject; those operations intentionally keep google in the default response. One current exception is scan: its top-level google, requests, and googleJson fields are gated, but its parsed entities and reviews retain their nested raw / rawSources fields even without --raw. --endpoints-only reduces any response to its identity and endpoint inventory.

The package-owned pnpm start CLI reports success and failure the same way, so one parse handles both:

pnpm start --operation hotel-prices
{
  "ok": false,
  "operation": "hotel-prices",
  "error": { "name": "Error", "message": "Unknown operation: hotel-prices. Supported: catalog, ..." }
}

Success prints { "ok": true, ... } on stdout; a failure prints { "ok": false, ... } on stderr and exits 1. A refusal from a Google RPC service carries error.service, error.status, and error.retryable; --raw adds error.stack. ok belongs to the CLI: the library throws instead, and errorEnvelope is exported for callers that want to render the same shape.

The separate npx website-api google-travel and ext run runners print the operation result directly, with no ok field. Their status and failure diagnostics use stderr; parse stdout without merging stderr into it.

test/contract.test.ts holds this contract to every one of the 29 operations, running the network ones against a stubbed transport: each names itself, reports endpoints of the documented shape, gates the top-level transport views as described above, and serializes without losing a value.

Hotel endpoint inventory

Google Hotels is a server-rendered/stateful frontend, not a stable REST API. The integration uses anonymous HTTP documents and direct batchexecute calls; it never launches Chrome or executes Google's JavaScript. It decodes raw f.req positional JSON and each wrb.fr response while retaining Google's arrays. MQghPc bootstraps destination, filter, record, and map state, AtySUc enriches a selected property, and canonical properties use /travel/hotels/entity/:entityId documents.

Private data calls are multiplexed through:

POST /_/TravelFrontendUi/data/batchexecute?rpcids=<RPC_ID>&...

Hotel flows observed through 2026-08-10 used these RPC IDs:

  • MQghPc — anonymous server-side destination, search, filter metadata, records, and map bootstrap
  • mejVKc — destination/property autocomplete
  • FCE32b — map geometry, bounds, and search state
  • AtySUc — map prices plus the selected property's full record: identity, coordinates, address, phone, nearby places, price, taxes, providers, rooms, reviews, topics, amenities, and photos
  • M0CRd — shared Travel hotel page data
  • yY52ce — per-hotel daily price calendar for a date range, length of stay, and currency
  • bdmBfe, ocp93e, zM1L7d — shared hotel entity/detail data
  • pSDzMb — entity photo/location-associated data

The typed entities, mapMarkers, supplementalReviews, and reviewPageTokens fields are derived only from Google's own payloads. --raw returns those payloads as well: google.requests[].calls[].payload and google.responses[].payload in Google's original JSON-array format, plus the f.req strings and full transport envelopes. --save-raw writes the decoded request and response JSON to an ignored artifact. A real Google Travel URL passed with --url is fetched directly. Hotel filter values are normalized without relying on the frontend's opaque ts, qs, or ap UI serialization; the enforcement boundary is described below.

rpcGroups names the RPCs that answered without carrying their payloads: one entry per RPC with its stable name, purpose, applicable surface and sections, source paths, and request/response counts. That makes AtySUc, M0CRd, VxbZPe, and the other multiplexed calls identifiable in every response. With --raw, google.byRpc adds the request payloads, response payloads, and summaries for the same grouping.

Several hotel operations project that one capture differently rather than returning it whole:

  • search, hotel-json, hotel-filters — the full search response: markers, property records, reviews, offers
  • hotel-map — markers only, plus the bounding box they occupy (derived from the markers, not a Google viewport)
  • hotel-insights — ratings, review distributions, review topics and summaries, price bands, and neighborhood context per property, without offers, photos, or raw records
  • detail, hotel-all — one property, optionally with every section and its price calendar
  • autocomplete — properties, places, and query suggestions
  • hotel-destinations — the places only, for turning a typed place name into a search query

Typed hotel search filters

search, hotel-json, and hotel-filters parse and echo every group in the published filter contract. The current result filter applies only minimum/maximum displayed price, guest rating, hotel class, and the lowest-price, highest-rating, and most-reviewed sorts to decoded map markers. relevance preserves Google's order. The remaining accepted fields are validated and echoed but do not currently remove or reorder results, so callers must not treat them as enforced constraints:

  • --sort: relevance, lowest-price, highest-rating, or most-reviewed
  • --min-price, --max-price: numeric amounts parsed from displayed nightly prices
  • --property-type: hotels or vacation-rentals
  • --free-cancellation, --special-offers
  • --guest-rating: any, 3.5, 4.0, or 4.5
  • --hotel-class: any combination of 2, 3, 4, and 5
  • --amenities: repeatable names/slugs such as free-wifi, pool, or fitness-center
  • --eco-certified
  • --brands: repeatable Google chain or subchain labels such as Hilton Honors

Call hotel-filter-options to retrieve the published filter vocabulary and a price ceiling derived from the current destination's returned markers. available.price.displayedMaximum is quoted verbatim from the highest-priced returned marker, in whatever currency Google returned it; --currency reaches the entity and calendar RPCs, not this search bootstrap. Sort, rating, class, property-type, room, offer, and amenity choices come from static package constants rather than destination discovery. In the current implementation, available.amenities is the combined global list: restrict it with predefined.amenitiesByPropertyType[propertyType] before submitting a value. available.brands is a short, hardcoded set of major loyalty programs, not an exhaustive list of chains available at the destination. Stable choices are also exported as HOTEL_SEARCH_FILTER_OPTIONS for schema and tool generation.

const choices = await executeOperation({
  operation: "hotel-filter-options",
  query: "Boston hotels",
});

// For amenities, intersect available.amenities with
// choices.predefined.amenitiesByPropertyType[choices.propertyType].

Repeat --hotel-class, --amenities, and --brands, or provide comma-separated values. Hotel-only offers, class, sustainability, and brand filters cannot be combined with property-type=vacation-rentals.

Vacation-rental endpoints and filters

Vacation rentals deliberately have separate discoverable operations while reusing the same lodging capture, validation, filtering, and RPC grouping code:

  • vacation-rental-catalog — document, section, transport, support endpoints, and labeled RPC inventory
  • vacation-rental-filter-options — the complete predefined contract plus destination-specific price cap
  • vacation-rental-search — filtered search and grouped RPC responses
  • vacation-rental-detail / vacation-rental-all — overview or all prices/reviews/photos/about/location sections
  • vacation-rental-payloads — decoded payloads and per-RPC summaries

The shared sort, price, and guest-rating constraints are enforced on decoded markers. Rental property type, bedroom, bathroom, and amenity values are validated and echoed, but they do not currently filter the returned markers. Their accepted values are predefined:

  • --rental-property-types: apartments, houses, cottages, villas, houseboats, other
  • --bedrooms: 0, 1, 2, 3, 4, or 5 (Google displays these as 0+ through 5+)
  • --bathrooms: 0, 1, 2, 3, 4, or 5
  • rental amenities: free-wifi, fitness-center, air-conditioned, kid-friendly, pool, pet-friendly, hot-tub, kitchen, patio-or-deck, outdoor-grill, crib, fireplace

The observed rental search RPCs are FCE32b, AtySUc, and M0CRd; entity sections use ocp93e, zM1L7d, yY52ce, and rental-specific photo RPC VxbZPe. Autocomplete uses mejVKc.

Property identity

Google Travel identifies one hotel by an entity token — the Ch… value in /travel/hotels/entity/<token>. It is a base64url protobuf pairing a numeric property id with that property's Knowledge Graph id:

{ 1: { 1: <propertyId>, 3: <knowledgeGraphId> }, 2: 1 }

autocomplete returns both members verbatim, so properties[].entityToken is composed rather than guessed, and parseHotelEntityToken decodes any token Google mints back into its two parts. Every hotel record found in a payload is recognised by decoding the token at its positional slot, which is why nested and duplicated copies of the same property collapse to one entry.

Property collections differ by operation; callers must not assume one shared key:

| Operation | Property collection | Entity-token path | |---|---|---| | autocomplete | properties[] | properties[].entityToken | | hotel-json, search, detail | entities[] | entities[].entityToken | | hotel-candidates | candidates[] | candidates[].entityToken | | hotel-map | mapMarkers[] | mapMarkers[].entityToken |

In JavaScript, check the applicable value with Array.isArray before mapping it. In particular, hotel-json has no properties field.

The other identifiers in a response address different things and are not interchangeable with the token: entities[].entityId and properties[].propertyId are numeric property ids, placeId is a Maps place, and /m/… or /g/… are Knowledge Graph ids. The yY52ce calendar accepts only the entity token and answers anything else with zero days, so hotel-calendar rejects the other forms with a message naming the right one instead of returning an empty result.

For reliable automation, call hotel-candidates --query <hotel name>, inspect its compact candidates[] response, confirm both the exact name and address, and pass that candidate's entityToken to hotel-calendar --entity-id. Do not redirect the candidate response to a temporary file, run a detached jq command, automatically select the first result, or reuse a token from documentation. The older hotel-calendar --query resolver remains for compatibility but is not an agent-safe workflow because Google autocomplete may return only destination or query suggestions.

hotel-calendar calls the raw yY52ce RPC using an anonymous HTTP POST. It does not launch Chrome, read cookies, fetch the hotel HTML, or require Google to establish a browser session. It returns one priceCalendar.days[] entry per check-in date with check-out date, length of stay, displayed nightly price, tax-inclusive price, numeric base amount, taxes, fixed fees, and total. Pass --raw to include each entry's original Google row. Rows Google returns with a date but no price are omitted. When Google has no daily prices for the property and date range, the operation still succeeds with availability.available: false, priceCalendar.available: false, and an empty priceCalendar.days array. hotel-all includes this same daily calendar alongside all entity sections.

yY52ce answers with prices only — no property name — so the operation pairs it with a token-only AtySUc lookup and reports property (entity token plus any resolved name, address, and hotel class) beside the prices. Those descriptive fields are nullable when Google does not return the entity record. The compatibility --query path first tries mejVKc, but automation should use hotel-candidates followed by --entity-id.

Both googleHotelCalendarHttpSite and the combined default googleTravelSite declare transport: "http". No operation calls ctx.browser() or ctx.network.capture().

When given an entity URL or --entity-id, detail fetches the entity document over HTTP, derives its title, resolves the matching result through MQghPc, and enriches it with AtySUc. A /travel/search URL instead uses its query and selects the lead result. hotel-all additionally fetches each entity-section document and the anonymous yY52ce price calendar when it can resolve an entity token.

The complete observed inventory, including shell, OneGoogle, Maps, and telemetry routes, is in src/google/catalog.ts. Telemetry is cataloged but is deliberately not replayed as a product API. scan deduplicates the raw requests by method, origin, path, and RPC ID and reports IDs missing from the catalog. flight-scan is its flight counterpart, but flights answer from named services rather than multiplexed RPC IDs, so it reports services and newServices against FLIGHT_SERVICE_CATALOG alongside rpcIds.

Flights endpoint inventory

Flights uses its own FlightsFrontendUi application. Initial and selected-leg searches read structured ds:1 shopping data from public Flights documents; date-grid and booking call named FlightsFrontendService endpoints; autocomplete uses the multiplexed H028ib batchexecute RPC:

POST /_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetShoppingResults
POST /_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetCalendarGrid
POST /_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetBookingResults
POST /_/FlightsFrontendUi/data/batchexecute?rpcids=H028ib

The three named service endpoints do not use the batchexecute envelope. Their body is a bare [null, "<json>"] pair in the same f.req form field, and the response is )]}' followed by one or more length-prefixed wrb.fr chunks. Those length headers count UTF-8 bytes, so decodeGoogleRpcStream reads the encoded buffer — airline and airport names contain non-ASCII text, and character offsets would desynchronise the stream. Autocomplete uses the normal batchexecute request and response envelope instead.

City-wide flight searches use Google's geographic entity, not a guessed metropolitan IATA code. Agents must run flight-autocomplete --query <city or metro>, match the intended candidate by name, city, and description, and pass that candidates[].value as --origin or --destination. For example, an NYC lookup returns New York with value: "/m/02_286" alongside the individual JFK, LGA, and EWR airport candidates. Each candidate includes { kind, name, city, description, code, entityId, value }. A direct, unambiguous airport IATA code needs no lookup.

Because the request carries the filters, Google applies them: stops, cabin, airlines and alliances (include and exclude), layover bounds, price cap, duration cap, emissions, bag counts, and sort order all come back applied, and the result set is the one the UI would show. One-way searches return real one-way fares — the previous document-scraping implementation could not price them at all and substituted round-trip totals.

A round trip lists outbound options first, exactly as the UI does; --select <n> encodes the chosen legs into Google's second public search document and returns its complete-itinerary return prices. flight-booking repeats that selection and then calls GetBookingResults, because Google identifies an itinerary by a booking token and by the chosen legs on each segment — a token alone returns no vendors. On a round trip it takes --select for the outbound and --select-return for the return leg; both default to the first option.

flight-dates prices a whole range through GetCalendarGrid, Google's date grid, and is the flight counterpart to the hotel price calendar. Google sweeps at most 61 days per request, so --calendar-start and --calendar-end are validated against that. The response carries dates[] of { departure, returnDate, price, currency } plus the cheapest entry. Round-trip grids require --return to establish a round-trip request, while --nights sets the stay length priced for every departure in the range.

Flight operations build requests from structured inputs. They do not accept the site's lodging-oriented --url parameter or a caller-supplied --tfs; the extension generates that state and fetches the corresponding public shopping document itself.

Pacing and refused calls

A refusal is not an empty result set. Google answers HTTP 200 with a wrb.fr row carrying no payload and a gRPC status instead — [["wrb.fr",null,null,null,null,[13]],["di",45],…], where 13 is INTERNAL. In the documented measurements Google did not use HTTP 429 for these refusals, although the retry policy also handles 429. Decoded naively, the status response can be mistaken for "no flights match" unless its status is checked.

Measured against the live service on 2026-08-11, in one session from one client and IP:

  • Short bursts are fine. A rested client answered 14 back-to-back requests in 2.6s (≈5.4 req/s), and a 150-request run at ≈2.5 req/s was answered 143 times, with refusals scattered rather than a cliff.
  • Within that window, refusal likelihood tracked request rate: roughly 1 in 1–7 at ≈4 req/s, 1 in 20 at ≈0.7 req/s, and each refusal was momentary — the next request ~2s later always succeeded.
  • After several hundred requests across the session, refusals became persistent rather than momentary, on every flight service at once, and outlasted the retry budget. So there does appear to be a longer-horizon limit; this measurement did not establish its size or its reset period, and the short-window numbers above should not be read as a licence to sustain that rate.

Treat the pacing defaults as a floor, not a ceiling — a datacenter IP or parallel callers will need slower settings, and heavy use should expect to back off for minutes rather than seconds.

postGoogleService raises GoogleRpcServiceError carrying the status. Google now gates anonymous flight services with a per-page BotGuard proof and reports a bare HTTP call as status 13 (INTERNAL). Initial flight searches therefore consume the structured ds:1 result rendered in the public Flights document; date-grid and other gated follow-ups fall back to a clean browser context so Google's own JavaScript creates the proof. Requests are still paced through a token bucket shared process-wide per frontend.

The bucket defaults to 2 requests/second with a burst of 8. Override with GOOGLE_TRAVEL_RPC_PER_SECOND and GOOGLE_TRAVEL_RPC_BURST. A persistent refusal is not something retry or pacing can fix from inside a single run — it needs a pause between runs.

Typed flight search filters

Call flight-filter-options to retrieve the machine-readable contract before constructing a search. Flight search supports round-trip and one-way dates, metro-area or airport origin/destination choices, all four cabins, structured passenger counts, stops, departure/arrival windows, airline and alliance inclusion or exclusion, layover duration, maximum price and duration, emissions, carry-on and checked bags, locale, currency, and deterministic sorting. Airline values are IATA codes published by the filter-options operation.

Cabin and passenger controls are encoded into Google's public tfs state. Stops, time windows, airline codes, layover bounds, duration, price, emissions, and sorting are applied to Google's complete structured initial result rows. --departure-window and --arrival-window constrain the outbound leg only.

--currency defaults to USD and is always sent. Google reads the currency off the request and infers it from the caller's IP when the request omits one, so an unset currency does not mean a default — it means whatever the host's egress looks like, and the same search answered USD from one machine and RUB from another. The default is applied when the filters are parsed rather than only declared as a flag, so a library caller through runSite/executeOperation and an HTTP-gateway caller get it too; both are handed the options they passed, and only the CLI fills declared defaults in. Every response reports the currency in effect under filters.currency.

The TypeScript API accepts the same uppercase/underscore aliases used by Fli, while returning normalized kebab-case values:

const flights = await executeOperation({
  operation: "flight-search",
  origin: "JFK",
  destination: "LAX",
  departure: "2026-08-20",
  tripType: "one-way",
  cabinClass: "business",
  adults: 2,
  children: 1,
  maxStops: "ONE_STOP",
  departureWindow: "6-20",
  airlines: ["DL"],
  excludeAlliances: ["STAR_ALLIANCE"],
  minLayover: 60,
  maxLayover: 360,
  sort: "duration",
  currency: "USD",
});

filters echoes the normalized request and url is the equivalent public Google Flights deep link. The initial flight-search operation fetches that document and reads its structured ds:1 data; it does not scrape rendered result text or CSS selectors.

Each itinerary is fully decoded: price and currency (the currency is a protobuf field inside Google's price token), durationMinutes, stops, selfTransfer, mixedCabin, per-leg airline, flightNumber, operatingAirline, airports and their full names, departure/arrival timestamps, aircraft, legroom, co2Grams, and amenities (wifi, power, on-demand video, legroom rating). Layovers carry airport, city, and wait time. emissions reports this itinerary's grams against the typical figure for the route.

A row whose price head is empty is Google declining to publish a list price — common for premium-cabin round trips — not a malformed row. Those itineraries are returned with price: null and a usable bookingToken, so flight-booking can still resolve real fares for them.

Coverage boundary

There is no finite, published Google Travel endpoint list. RPC IDs, build labels, opaque page-state tokens, experiments, locale-specific calls, signed-in features, and telemetry can change without notice. The catalog therefore distinguishes the routes observed in the exercised hotel and flight flows from dynamic scan output. Run both HTTP scanners for the locale and request variants you need before relying on the inventory. “Complete” here means every route observed across those exercised flows; no client can guarantee undiscoverable experiment or account-only routes that Google did not serve.