FlightAware Live Flight Tracking
Purpose
Given a flight identifier (airline ICAO + flight number like UAL1, IATA-style like UA1, or an aircraft registration / tail number like N819NW) and an optional date, return the live flight status from FlightAware: current state (Scheduled / Airborne / Taxiing / Landed / Cancelled / Diverted / Result Unknown), both origin and destination airports (IATA + ICAO + name + city + terminal + gate when shown), all available gate / takeoff / landing times (scheduled / estimated / actual, in local airport time + UTC), aircraft type and tail number (when not paywalled), the filed ATC route, waypoint polyline, the current position (lat/lng, altitude, ground speed, heading) for en-route flights, delay minutes, and the canonical FlightAware permalink. Read-only — never clicks Sign In, Subscribe, or any track-alert / set-up-alert control.
When to Use
- "Where is flight UA1 right now?" / "What's the status of BA286?" / "Is AAL2360 on time?"
- A trip-management agent polling for arrival ETA changes.
- An aircraft-history lookup keyed by tail (e.g., "what is N819NW currently flying?").
- Any scheduling, baggage-claim, or pickup workflow that needs a live status + ETA without paying AeroAPI's $100/mo minimum.
Workflow
FlightAware's /live/flight/{ident} page is server-rendered with a trackpollBootstrap JavaScript global that contains every datum the consumer page renders — origin, destination, all four time blocks (gateDepartureTimes, takeoffTimes, landingTimes, gateArrivalTimes), aircraft.type / friendlyType / tail, flightStatus, coord / heading / groundspeed / altitude, flightPlan.route, waypoints[] (filed route polyline), track[] (position history), and a fully-populated links block (permanent, trackLog, flightHistory, operated, etc.). This is the AeroAPI payload shipped to the browser for free — a single browse cloud fetch --proxies call is enough to extract the entire flight snapshot. There is no need to drive a real browser session for read-only tracking.
Browserbase residential proxies are mandatory. Without --proxies, FlightAware returns 402 Payment Required with an empty body to bare Browserbase / AWS egress IPs. The --verified (stealth) flag is also recommended for any CDP-driven fallback path — FlightAware does TLS / header / JS fingerprinting on the bot-detection layer for non-fetch traffic.
-
Normalize the input identifier.
- Trim whitespace, uppercase.
- If the input is
<2-letter IATA> + digits(UA1,DL47,AA100,BA286), expand to ICAO before constructing the URL./live/flight/UA1returns the Unknown Flight page;/live/flight/UAL1works. Common map:UA→UAL,AA→AAL,DL→DAL,BA→BAW,LH→DLH,AF→AFR,KL→KLM,QF→QFA,SQ→SIA,JL→JAL,NH→ANA,EK→UAE,QR→QTR,WN→SWA,B6→JBU,AS→ASA,F9→FFT,NK→NKS,AC→ACA. For airlines not in this list, look up the ICAO code in the response'sf.iataIdent/f.identpair after a successful fetch (FlightAware bidirectionally maps them). - Registration / tail numbers (
N12345,N819NW,G-XLEA,VH-OQA) are passed verbatim. - Free-form input that does not fit either shape: do NOT call
/live/findflight?ident=...— that endpoint returns the Flight Finder UI HTML, not a flight resolution. Either expand to ICAO yourself or give up.
-
Construct the URL.
- Live / soonest leg:
https://www.flightaware.com/live/flight/{IDENT} - Specific historical leg (when you have the permalink):
https://www.flightaware.com/live/flight/{IDENT}/history/{YYYYMMDD}/{HHMM}Z/{ORIG_ICAO}/{DEST_ICAO}
- Live / soonest leg:
-
Fetch with proxies.
browse cloud fetch "https://www.flightaware.com/live/flight/UAL1" \ --proxies --allow-redirects --output /tmp/fa.html200 OK → continue. 402 → proxies are off or the proxy pool is empty; retry. 403 / challenge HTML → fall back to the Browser fallback below.
-
Extract
trackpollBootstrapfrom the HTML. Locate the literaltrackpollBootstrap = {, then bracket-match through nested objects and JS-quoted strings to find the closing}. The regex/trackpollBootstrap = (\{[\s\S]*?\});\n/will under-match and/...;\s*var/will over-match into the adjacenttrackpollGlobalsblock — use a real bracket-matcher (handle"...",\",{,}). Once parsed:const obj = JSON.parse(rawBootstrap); const fid = Object.keys(obj.flights)[0]; // single flight per page const f = obj.flights[fid]; -
Branch on the bootstrap shape.
f.unknown === true— the ident is not in FlightAware's index.f.unknownContentis a snippet of HTML containing the message"FlightAware couldn't find flight tracking data for <em>{IDENT}</em> just yet."Return{success: false, reason: "ident_not_found", message: stripHtml(f.unknownContent)}.f.blocked === true— the aircraft owner has hidden tracking. Return{success: false, reason: "blocked", message: f.blockMessage}.- Otherwise the bootstrap is a full flight record (see step 6).
-
Map fields to the output schema.
Output field Bootstrap path Notes identf.identICAO format (e.g. UAL1).ident_iataf.iataIdentIATA format (e.g. UA1). May benullfor GA.friendly_identf.friendlyIdent"United 1". statederive from f.flightStatus+ flags"scheduled" / "taxiing" / "airborne" / "arrived" / "" → empty string means scheduled-but-not-yet-active. f.cancelled,f.diverted,f.resultUnknownoverride.origin.iata/icao/name/city/gate/terminal/tzf.origin.{iata,icao,friendlyName,friendlyLocation,gate,terminal,TZ}TZis prefixed with a colon (":America/Los_Angeles") — strip it.friendlyNamehas typographic apostrophes (San Francisco Int'l).destination.*f.destination.*Same shape as origin. departure.scheduled/estimated/actualf.gateDepartureTimes.{scheduled,estimated,actual}Unix epoch seconds. Multiply by 1000 for JS Date. Convert to local using f.origin.TZ.takeoff.scheduled/estimated/actualf.takeoffTimes.*Distinct from gate departure — these are wheels-up times. landing.scheduled/estimated/actualf.landingTimes.*Wheels-down. arrival.scheduled/estimated/actualf.gateArrivalTimes.*Gate arrival. aircraft.typef.aircraft.type(ICAO type code, e.g.B789)aircraft.friendly_typef.aircraft.friendlyType"Boeing 787-9 Dreamliner (twin-jet)". aircraft.registrationf.aircraft.tailMay be nullwhenf.redactedTail === true— see Gotchas.flight_plan.route_textf.flightPlan.routeATC route string ( "GNNRR3 ALCOA 3800N/13000W ... KARTO2B"). Split on whitespace for waypoint identifiers.flight_plan.distance_nmf.flightPlan.directDistanceandf.flightPlan.plannedDistanceTwo values — direct great-circle vs filed. flight_plan.ete_secondsf.flightPlan.eteEstimated time enroute, seconds. flight_plan.filed_speed_kt / filed_altitude_flf.flightPlan.speed,f.flightPlan.altitudeAltitude in flight levels (e.g. 400= FL400 = 40 000 ft).waypoints(filed route polyline)f.waypoints[]Array of [lng, lat]pairs.position.lat/lngf.track[f.track.length-1].coord[lng, lat]— swap for{lat, lng}output. Top-levelf.coordis usuallynull; always read the lasttrack[]entry.position.altitude_ftf.track[last].alt * 100Stored as flight-level / 100 ft. position.ground_speed_ktf.track[last].gsor top-levelf.groundspeedposition.heading_degf.headingposition.timestampf.track[last].timestampUnix epoch seconds. delay_minutes(f.gateArrivalTimes.estimated - f.gateArrivalTimes.scheduled) / 60Compute client-side; bootstrap does not pre-compute it. Negative = early. urlhttps://www.flightaware.com/live/flight/{f.ident}permalinkhttps://www.flightaware.com${f.links.permanent}The history-specific URL with date + airports. track_log_urlhttps://www.flightaware.com${f.links.trackLog}Useful for richer position history. -
f.activityLog.flights[]is a back-history list of recent legs flown by the same airline ident (when fetched by ident) or by the same aircraft (when fetched by tail). Index[0]is the current/upcoming leg; subsequent entries are past legs in reverse-chronological order. Each leg has its ownpermaLinkpointing to a date-specific/history/...URL. Use this to satisfy an optionaldateinput parameter — match the date inpermaLink(/history/YYYYMMDD/...) to the user's requested date, then refetch the specific permalink for the per-leg detail. -
For the optional
dateparameter, after step 6, scanf.activityLog.flights[]for the entry whosepermaLinkcontains/history/{YYYYMMDD}/. If found, refetchhttps://www.flightaware.com${that.permaLink}and parse itstrackpollBootstrapthe same way — that page'sflights[fid]will be the date-specific leg (richer than the activityLog summary). If no entry matches, return{success: true, state: "no_leg_on_date", available_dates: [...permaLink date prefixes]}rather than emitting an unrelated leg. -
Done — no session to release.
browse cloud fetchis a stateless API call. Skip thesessions updatestep entirely.
Browser fallback
If browse cloud fetch --proxies is failing (rare — proxy pool exhausted, FlightAware blocks the residential pool entirely, etc.), fall back to a Browserbase CDP session with --verified --proxies:
sid=$(browse cloud sessions create --keep-alive --verified --proxies | jq -r .id)
export BROWSE_SESSION="$sid"
browse open "https://www.flightaware.com/live/flight/UAL1" --remote
browse wait load
browse wait timeout 2500 # let the bootstrap script tag emit
browse get html body > /tmp/fa.html # bootstrap is in the inline <script>
# parse trackpollBootstrap exactly as above
browse cloud sessions update "$sid" --status REQUEST_RELEASE
The page does have <title> text, an accessibility tree with refs, and visible time / status text, so browse snapshot + ref-driven extraction also works — but it is far more expensive than parsing the bootstrap JSON.
Site-Specific Gotchas
browse cloud fetchwithout--proxiesreturns HTTP402with an empty body — FlightAware hard-blocks bare AWS / cloud egress IPs at the WAF layer. Residential proxies are non-optional. Recovery: re-run with--proxies; do NOT retry without proxies — it is a stable block, not a transient./live/findflight?ident=...is not a search API — it serves the Flight Finder HTML form. Do not use it to resolve free-form input. Either expand IATA → ICAO yourself client-side, or look up the ICAO airline code in a static table.- IATA-format airline idents (
UA1,BA286) return the Unknown Flight page when used as the URL slug — FlightAware's/live/flight/{ident}requires ICAO format (UAL1,BAW286). Always normalize before fetching. - Tail-number redaction. When the URL is keyed by airline ident (e.g.
/live/flight/UAL1), the bootstrap hasaircraft.tail: nullandredactedTail: true— the assigned aircraft tail is paywalled. When the URL is keyed by the tail itself (e.g./live/flight/N819NW), the bootstrap hasaircraft.tail: "N819NW"andredactedTail: null— registration is freely returned. So: if the user wants the tail and only knows the airline flight number, you cannot return it from the unauthenticated page. Document this in the response:aircraft.registration: null, aircraft.registration_redacted: true. - The
trackpollBootstrapblock is JavaScript, not JSON. It is emitted asvar trackpollBootstrap = {...};followed by another script (trackpollGlobals). A naïve regex like/trackpollBootstrap = (\{[\s\S]*?\});/greedy-matches into the next variable. Use a proper bracket-matcher that respects string boundaries ("...",\",{,}). One reference implementation is included in the workflow section. f.coordis usuallynulleven for airborne flights. The current position lives atf.track[f.track.length - 1].coord([lng, lat]), not at the top-levelf.coord. The top-levelf.heading,f.groundspeed,f.altitudeare populated — only the coord is buried in the track array.coordarrays inf.track,f.waypoints,f.origin.coord,f.destination.coordare[lng, lat](not[lat, lng]). Swap for the conventional{lat, lng}output shape.f.altitudeandtrack[].altare in flight levels (hundreds of feet).400 = FL400 = 40 000 ft. Multiply by 100 for feet.- Times are Unix epoch seconds (
takeoffTimes.scheduled: 1779083400→ multiply by 1000 for JS Date). Convert to local airport time usingorigin.TZ/destination.TZ, which are prefixed with a colon (":America/Los_Angeles") — strip the leading colon before passing to a TZ library. f.flightStatusvalues seen in the wild:"airborne","scheduled","arrived",""(empty string for not-yet-active future legs),"taxiing". The status string is lowercased and not necessarily comprehensive — combine withf.cancelled(bool),f.diverted(bool),f.resultUnknown(bool),f.historical(bool) to derive a canonical state.- Unknown ident →
f.unknown: true, NOT an HTTP 404. The page returns 200 OK withtitle='Unknown Flight - FlightAware'. The bootstrap still parses; checkf.unknownbefore reading other fields. f.activityLog.flights[]indices —[0]is the next-upcoming or current leg; subsequent indices are past legs in reverse-chronological order. The most-recent flown leg is[1]when there's an upcoming one,[0]when there isn't. Match by theYYYYMMDDsubstring inpermaLinkto be safe.- AeroAPI is not a free fallback. FlightAware's developer-facing AeroAPI has a $100/month minimum on the Personal tier and a stricter rate limit. The consumer page bootstrap exposes most of the same data — prefer it for read-only / low-volume use. Do not advise users to "just use AeroAPI" without flagging the cost floor.
- No CDP /
browse open --remotework from network-restricted sandboxes. Sandboxes that allowlist onlyapi.browserbase.com(e.g. some Vercel Sandbox configs) can usebrowse cloud fetchbut cannot reachconnect.usw2.browserbase.comfor live CDP sessions. The Fetch API is the working path under that constraint — and is also cheaper and faster. - The
links.registrationURL is not the aircraft owner registration data — it's the URL slug for the same history leg with the tail substituted into the path. It is empty (/live/flight//history/...) whenaircraft.tailis redacted. - Encrypted flight IDs.
f.encryptedFlightId(64-char hex) is FlightAware's internal handle for cross-page references. Do not parse it; treat as opaque. trackpollGlobals.TOKENis a CSRF-like token that authorizes the trackpoll WebSocket / poll endpoint for live updates after page load. It is single-use per page and not needed for one-shot snapshot extraction. Do not attempt to call the poll endpoint with it — the WebSocket protocol is undocumented and bot-detected separately.redactedCallsignis a separate flag fromredactedTail. For some flights the ATC callsign is hidden too. Surface both in the output for transparency.
Expected Output
Live / airborne (canonical happy-path):
{
"success": true,
"ident": "UAL1",
"ident_iata": "UA1",
"friendly_ident": "United 1",
"state": "airborne",
"cancelled": false,
"diverted": false,
"result_unknown": false,
"delay_minutes": 3,
"origin": {
"iata": "SFO",
"icao": "KSFO",
"name": "San Francisco Int'l",
"city": "San Francisco, CA",
"terminal": "3",
"gate": "F11",
"timezone": "America/Los_Angeles",
"coord": { "lat": 37.6188, "lng": -122.3754 }
},
"destination": {
"iata": "SIN",
"icao": "WSSS",
"name": "Singapore Changi",
"city": "Singapore",
"terminal": "2",
"gate": "E28",
"timezone": "Asia/Singapore",
"coord": { "lat": 1.3557, "lng": 103.9875 }
},
"departure": {
"scheduled_utc": "2026-05-18T05:40:00Z",
"estimated_utc": "2026-05-18T06:00:00Z",
"actual_utc": "2026-05-18T05:57:00Z"
},
"takeoff": {
"scheduled_utc": "2026-05-18T05:50:00Z",
"estimated_utc": "2026-05-18T06:28:00Z",
"actual_utc": "2026-05-18T06:28:00Z"
},
"landing": {
"scheduled_utc": "2026-05-18T22:08:00Z",
"estimated_utc": "2026-05-18T22:39:00Z",
"actual_utc": null
},
"arrival": {
"scheduled_utc": "2026-05-18T22:25:00Z",
"estimated_utc": "2026-05-18T22:28:00Z",
"actual_utc": null
},
"aircraft": {
"type": "B789",
"friendly_type": "Boeing 787-9 Dreamliner (twin-jet)",
"registration": null,
"registration_redacted": true
},
"flight_plan": {
"route_text": "GNNRR3 ALCOA 3800N/13000W 3800N/14000W 3600N/15000W ... KARTO2B",
"route_waypoints": ["GNNRR3", "ALCOA", "3800N/13000W", "..."],
"filed_speed_kt": 497,
"filed_altitude_ft": 40000,
"direct_distance_nm": 7342,
"planned_distance_nm": 7690,
"ete_seconds": 58680
},
"waypoints_polyline": [
{ "lat": 37.62, "lng": -122.38 },
{ "lat": 37.67, "lng": -122.50 }
],
"position": {
"lat": 16.6833,
"lng": 131.6833,
"altitude_ft": 40000,
"ground_speed_kt": 463,
"heading_deg": 270,
"timestamp_utc": "2026-05-18T17:14:44Z"
},
"url": "https://www.flightaware.com/live/flight/UAL1",
"permalink": "https://www.flightaware.com/live/flight/UAL1/history/20260518/0550Z/KSFO/WSSS",
"track_log_url": "https://www.flightaware.com/live/flight/UAL1/history/20260518/0550Z/KSFO/WSSS/tracklog"
}
Scheduled / future leg (no position, actual_* all null, state: "scheduled"):
{ "success": true, "ident": "UAL1", "state": "scheduled", "position": null,
"departure": { "scheduled_utc": "...", "estimated_utc": "...", "actual_utc": null },
"arrival": { "scheduled_utc": "...", "estimated_utc": "...", "actual_utc": null },
"...": "remaining fields same shape as above" }
Landed / arrived (state: "arrived", all actual_* populated):
{ "success": true, "ident": "UAL1", "state": "arrived", "position": null,
"departure": { "actual_utc": "..." },
"arrival": { "actual_utc": "..." },
"...": "remaining fields same shape" }
Cancelled:
{ "success": true, "ident": "UAL47", "state": "cancelled", "cancelled": true,
"origin": { ... }, "destination": { ... },
"departure": { "scheduled_utc": "..." }, "arrival": { "scheduled_utc": "..." },
"position": null }
Diverted (state: "diverted", destination is the diversion airport — see permalink for the planned destination):
{ "success": true, "ident": "AAL360", "state": "diverted", "diverted": true,
"origin": { ... }, "destination": { ... (diversion airport) },
"departure": { ... }, "arrival": { ... },
"position": { ... } }
Ident not found:
{ "success": false, "reason": "ident_not_found", "ident": "FOO9999",
"message": "FlightAware couldn't find flight tracking data for FOO9999 just yet. Please double-check the flight number/identifier..." }
Aircraft owner has blocked tracking:
{ "success": false, "reason": "blocked", "ident": "N12345",
"message": "The owner of this aircraft has requested that the flight not be publicly tracked." }
Result unknown (FlightAware lost contact / position-only data):
{ "success": true, "ident": "UAL1", "state": "result_unknown", "result_unknown": true,
"origin": { ... }, "destination": { ... },
"message": "Tracking data for this position-only flight is incomplete and potentially inaccurate." }
WAF / proxy block (rare with --proxies; only when the proxy pool itself is exhausted or banned):
{ "success": false, "reason": "blocked_upstream",
"ident": "UAL1",
"message": "FlightAware returned HTTP 402/403 to the proxy egress IP. Retry with a fresh proxy region or switch to the CDP fallback with --verified --proxies." }