Skip to content

Guide

Google Flights URL parameters, decoded

Copy a URL out of Google Flights after setting up a search and you get something like:

https://www.google.com/travel/flights?tfs=CBwQAhopEgoyMDI2LTEwLTE1...&curr=USD

Everything you clicked (origin, destination, dates, passengers, cabin, stops) is in there. It is just not in there in a form meant for you to read. This guide covers what each parameter does, how far you can get decoding the big one, and where the honest limits are.

The three parameters that matter

tfs= is the search itself. It is a protocol-buffer message, serialized and base64url-encoded. One opaque blob encodes the full flight query: the legs (origin, destination, date: one leg for a one-way, two for a round-trip), passenger counts, cabin, and filters like stops and airlines. When Google Flights loads a URL with tfs=, it reconstructs the entire search UI state from this blob.

q= is the informal cousin: a free-text query, the same thing you could type into the search box. ?q=flights to tokyo opens Google Flights with the query parsed out of natural language. It is convenient for hand-written links, and useless for precision: you cannot pin dates, filters, or a specific itinerary with it the way tfs= does.

curr= sets the display currency (curr=USD, curr=EUR). It rides alongside tfs= rather than inside it, which is occasionally useful: you can take an existing deep link and re-point the currency without touching the blob.

Decoding tfs= at the wire level

You do not need Google's schema to inspect a tfs= blob, because protobuf's wire format is self-describing down to field numbers and wire types (though not field names or meanings). Two steps:

1. Undo the base64url. The blob uses the URL-safe alphabet (- and _ instead of + and /) and typically arrives without padding, so pad it back up before decoding:

import base64, sys
from urllib.parse import urlparse, parse_qs

url = sys.argv[1]
tfs = parse_qs(urlparse(url).query)["tfs"][0]
raw = base64.urlsafe_b64decode(tfs + "=" * (-len(tfs) % 4))
print(raw.hex())

2. Walk the protobuf wire format. Every field in the byte stream is a varint tag (field number shifted left three bits, ORed with a wire type) followed by a payload whose shape the wire type determines: varint, 64-bit, length-delimited, or 32-bit. Length-delimited fields are where the interesting things live: strings like airport codes and dates are directly readable, and nested messages (the legs) decode recursively. If you have protoc installed, it will do the walk for you:

python3 -c "
import base64, sys
tfs = sys.argv[1]
sys.stdout.buffer.write(base64.urlsafe_b64decode(tfs + '=' * (-len(tfs) % 4)))
" "$TFS_BLOB" | protoc --decode_raw

--decode_raw prints the field-number tree with strings legible. You will spot IATA codes and YYYY-MM-DD dates immediately. What you will not get is names or guarantees: field 2 is just "field 2," and nothing promises it means the same thing next quarter.

If you would rather not install anything, the Google Flights URL parser on this site does the base64url + wire-format walk client-side in your browser: paste a URL, see the decoded tree.

The honest caveats

  • The format is undocumented. Google publishes no schema for tfs= and owes nobody stability. Everything decoded from it is inference from observed bytes.
  • Reading is safer than writing. Decoding a blob you already have degrades gracefully: worst case, a field you relied on moves and your parser shows an unknown. Constructing tfs= blobs yourself is the fragile direction: an encoding quirk you did not reproduce, or a silent format revision, produces links that open a wrong or empty search, and nothing errors to tell you.
  • A decoded URL is not an API. Even a perfect decoder only tells you what a search asks; it gets you no prices. To act on the data you still need something that returns fares.

The main practical reason people try to encode tfs= themselves is to build "book this" links programmatically. That problem is already solved from the other direction: every result returned by the Google Flights Live API carries a buy_link, a working deep link into Google Flights for that exact itinerary, with the blob already encoded by the same pipeline that read the fare:

{
  "price": "$56",
  "price_as_number": 56,
  "airline": "easyJet",
  "buy_link": "https://www.google.com/travel/flights?tfs=...&curr=usd"
}

So the division of labour that actually works is: decode URLs when a user hands you one (the parser tool or the snippet above), and never encode them. Search via the API and pass along the buy_link it returns per itinerary. You get the deep link without owning the format risk, and the same response carries the price context (price_insights_low/high and Google's low | typical | high verdict) that the URL alone never had. The one-way endpoint documents the full response shape.

Skip the blob entirely

Search by route and date, get fares with a working buy_link on every result. The deep link is built for you.

Free tier: 10 requests/month. No card to try.