flightaware.com

track-flight

Installation

Adds this website's skill for your agents

 

Summary

Look up live FlightAware status for any flight identifier (airline ICAO + number, IATA-style, or tail/registration). Returns origin and destination with terminal/gate, scheduled / estimated / actual gate-departure / takeoff / landing / gate-arrival times in local + UTC, aircraft type, route, current position (lat/lng, altitude, speed, heading), delay, and the permalink. Read-only.

FIG. 01
FIG. 02
FIG. 03
FIG. 04
FIG. 05
SKILL.md
317 lines

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.

  1. 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/UA1 returns the Unknown Flight page; /live/flight/UAL1 works. 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's f.iataIdent / f.ident pair 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.
  2. 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}
  3. Fetch with proxies.

    browse cloud fetch "https://www.flightaware.com/live/flight/UAL1" \
      --proxies --allow-redirects --output /tmp/fa.html
    

    200 OK → continue. 402 → proxies are off or the proxy pool is empty; retry. 403 / challenge HTML → fall back to the Browser fallback below.

  4. Extract trackpollBootstrap from the HTML. Locate the literal trackpollBootstrap = {, 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 adjacent trackpollGlobals block — 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];
    
  5. Branch on the bootstrap shape.

    • f.unknown === true — the ident is not in FlightAware's index. f.unknownContent is 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).
  6. Map fields to the output schema.

    Output fieldBootstrap pathNotes
    identf.identICAO format (e.g. UAL1).
    ident_iataf.iataIdentIATA format (e.g. UA1). May be null for 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.resultUnknown override.
    origin.iata/icao/name/city/gate/terminal/tzf.origin.{iata,icao,friendlyName,friendlyLocation,gate,terminal,TZ}TZ is prefixed with a colon (":America/Los_Angeles") — strip it. friendlyName has 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 null when f.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.directDistance and f.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-level f.coord is usually null; always read the last track[] entry.
    position.altitude_ftf.track[last].alt * 100Stored as flight-level / 100 ft.
    position.ground_speed_ktf.track[last].gs or top-level f.groundspeed
    position.heading_degf.heading
    position.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.
  7. 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 own permaLink pointing to a date-specific /history/... URL. Use this to satisfy an optional date input parameter — match the date in permaLink (/history/YYYYMMDD/...) to the user's requested date, then refetch the specific permalink for the per-leg detail.

  8. For the optional date parameter, after step 6, scan f.activityLog.flights[] for the entry whose permaLink contains /history/{YYYYMMDD}/. If found, refetch https://www.flightaware.com${that.permaLink} and parse its trackpollBootstrap the same way — that page's flights[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.

  9. Done — no session to release. browse cloud fetch is a stateless API call. Skip the sessions update step 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 fetch without --proxies returns HTTP 402 with 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 has aircraft.tail: null and redactedTail: true — the assigned aircraft tail is paywalled. When the URL is keyed by the tail itself (e.g. /live/flight/N819NW), the bootstrap has aircraft.tail: "N819NW" and redactedTail: 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 trackpollBootstrap block is JavaScript, not JSON. It is emitted as var 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.coord is usually null even for airborne flights. The current position lives at f.track[f.track.length - 1].coord ([lng, lat]), not at the top-level f.coord. The top-level f.heading, f.groundspeed, f.altitude are populated — only the coord is buried in the track array.
  • coord arrays in f.track, f.waypoints, f.origin.coord, f.destination.coord are [lng, lat] (not [lat, lng]). Swap for the conventional {lat, lng} output shape.
  • f.altitude and track[].alt are 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 using origin.TZ / destination.TZ, which are prefixed with a colon (":America/Los_Angeles") — strip the leading colon before passing to a TZ library.
  • f.flightStatus values 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 with f.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 with title='Unknown Flight - FlightAware'. The bootstrap still parses; check f.unknown before 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 the YYYYMMDD substring in permaLink to 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 --remote work from network-restricted sandboxes. Sandboxes that allowlist only api.browserbase.com (e.g. some Vercel Sandbox configs) can use browse cloud fetch but cannot reach connect.usw2.browserbase.com for live CDP sessions. The Fetch API is the working path under that constraint — and is also cheaper and faster.
  • The links.registration URL 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/...) when aircraft.tail is 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.TOKEN is 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.
  • redactedCallsign is a separate flag from redactedTail. 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." }
FlightAware Live Flight Tracking · browse.sh