Files
Flights/AIRLINE_API_SPEC.md
Trey T 847000d059 Land local WIP on top of JSX rewrite + wire JSXWebViewFetcher into target
Resolves the working tree that was sitting uncommitted on this machine
when the JSX rewrite (77c59ce, c9992e2) landed on the gitea remote.

- Adds favorites flow (FavoriteRoute model, FavoritesManager service,
  ContentView favorites strip with context-menu remove).
- Adds FlightLoad model + FlightLoadDetailView sheet rendering cabin
  capacity, upgrade list, standby list, and seat-availability summary.
- Adds WebViewFetcher (the generic WKWebView helper used by the load
  service for non-JSX flows).
- Adds RouteMapView for destination map mode and threads it into
  DestinationsListView with a list/map toggle.
- Adds AIRLINE_API_SPEC.md capturing the cross-airline load API surface.
- Wires JSXWebViewFetcher.swift into the Flights target in
  project.pbxproj (file was added to the repo by the JSX rewrite commit
  but never registered with the Xcode target, so the build was broken
  on a fresh checkout).
- Misc Airport/AirportDatabase/FlightsApp/FlightScheduleRow/
  RouteDetailView tweaks that the rest of this WIP depends on.

Build verified clean against the iOS Simulator destination.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:55:15 -05:00

21 KiB

Airline Flight Load API Specification

Integration-ready API reference for querying flight loads, standby lists, and seat availability across 7 airlines. All endpoints confirmed working as of April 2026.


Architecture Overview

All airlines except Spirit require Playwright (headless browser) to bypass Akamai TLS fingerprinting. The pattern is:

  1. Navigate to the airline's domain to establish a browser session
  2. Use page.evaluate(async () => { const r = await fetch(...); return r.json(); }) to call APIs from within the browser context

Spirit works with plain HTTP requests (curl/fetch).


UNITED AIRLINES

Base Setup

Domain: https://www.united.com
Auth: Anonymous token (no login required)
Method: Playwright → navigate to united.com → page.evaluate(fetch)

Step 1: Get Token

GET /api/auth/anonymous-token
Headers: none required

Response:

{
  "data": {
    "token": {
      "hash": "DAAAA...",
      "expiresAt": "2026-04-08T19:42:57Z"
    }
  }
}

Token is valid ~30 minutes. Refresh as needed.

Step 2: Search Flights by Route

GET /api/flightstatus/status/{flightNumber}/{date}?carrierCode=UA&useLegDestDate=true

Path params:
  flightNumber: "2238"
  date: "2026-04-08"

Headers:
  x-authorization-api: bearer {token.hash}
  Accept: application/json

Response: Flight status with departure/arrival times, gates, terminals, aircraft type, tail number, delays.

Step 3: Get Flight Loads + Standby List

GET /api/flightstatus/upgradeListExtended?flightNumber={num}&flightDate={YYYY-MM-DD}&fromAirportCode={origin}

Headers:
  x-authorization-api: bearer {token.hash}
  Accept: application/json

Response:

{
  "segment": {
    "airlineCode": "UA",
    "flightNumber": 2238,
    "flightDate": "20260408",
    "departureAirportCode": "EWR",
    "arrivalAirportCode": "LAX",
    "equipmentDescriptionLong": "Boeing 777-200ER",
    "departed": false
  },
  "pbts": [
    {
      "cabin": "Front",
      "capacity": 50,
      "authorized": 50,
      "booked": 50,
      "held": 0,
      "reserved": 0,
      "revenueStandby": 0,
      "waitList": 0,
      "jump": 0,
      "group": 0,
      "ps": 1,
      "sa": 5
    },
    {
      "cabin": "Middle",
      "capacity": 24,
      "authorized": 24,
      "booked": 16,
      "held": 0,
      "reserved": 0,
      "revenueStandby": 0,
      "waitList": 0,
      "jump": 0,
      "group": 0,
      "ps": 0,
      "sa": 0
    },
    {
      "cabin": "Rear",
      "capacity": 202,
      "authorized": 202,
      "booked": 164,
      "held": 0,
      "reserved": 0,
      "revenueStandby": 2,
      "waitList": 0,
      "jump": 0,
      "group": 0,
      "ps": 0,
      "sa": 4
    }
  ],
  "checkInSummaries": [
    {
      "cabin": "Front",
      "capacity": 50,
      "total": 50,
      "etktPassengersCheckedIn": 50,
      "revStandbyCheckedInWithoutSeats": 0,
      "nonRevStandbyCheckedInWithoutSeats": 0,
      "children": 0,
      "infants": 0,
      "bags": 0
    }
  ],
  "numberOfCabins": 3,
  "front": {
    "cleared": [
      {
        "currentCabin": "Front",
        "bookedCabin": "Rear",
        "firstName": "T",
        "lastName": "JEN",
        "passengerName": "T/JEN",
        "seatNumber": "1G",
        "clearanceType": "Upgrade",
        "skipped": false
      }
    ],
    "standby": []
  }
}

Field Reference

Field Description
pbts[].cabin "Front" (First/Polaris), "Middle" (Premium+), "Rear" (Economy)
pbts[].capacity Total seats in cabin
pbts[].booked Seats sold/assigned
pbts[].revenueStandby Revenue standby passengers
pbts[].sa Space available (non-rev standby)
pbts[].ps Positive space
pbts[].waitList Waitlisted passengers
front.cleared[] Passengers cleared for upgrade
front.standby[] Passengers on standby

Derived Values

availableSeats = capacity - booked
loadFactor = booked / capacity

AMERICAN AIRLINES

Base Setup

Domain: https://cdn.flyaa.aa.com
Auth: None required
Method: Playwright → set mobile UA headers → navigate to cdn.flyaa.aa.com → page.evaluate(fetch)

Required context headers (set via page.context().setExtraHTTPHeaders):
  User-Agent: Android/2025.31 Pixel 7|14|1080|2400|1.0|AmericanAirlines
  x-clientid: MOBILE
  Accept: application/json
  Content-Type: application/json
  Device-ID: {any-uuid}

Step 1: Search Flights by Route

GET /apiv2/mobile-flifo/flightSchedules/v1.0?origin={ORIG}&destination={DEST}&departureDay={D}&departureMonth={M}&searchType=schedule&noOfFlightsToDisplay=20

Response:

{
  "flightSchedules": {
    "flights": [
      [{
        "flightKey": "AA:3390:2026-04-09:DFW:0",
        "operatingCarrierCode": "AA",
        "operatingCarrierName": "AMERICAN EAGLE",
        "marketingCarrierCode": "AA",
        "flightNumber": "3390",
        "originAirportCode": "DFW",
        "originCity": "Dallas/ Fort Worth",
        "destinationAirportCode": "IAH",
        "destinationCity": "Houston",
        "departDate": "2026-04-09T07:01:00.000-05:00",
        "arrivalDate": "2026-04-09T08:20:00.000-05:00",
        "showUpgradeStandbyList": false,
        "allowFSN": false
      }]
    ]
  }
}

Step 2: Get Waitlist + Available Seats

GET /api/mobile/loyalty/waitlist/v1.2?carrierCode=AA&flightNumber={NUM}&departureDate={YYYY-MM-DD}&originAirportCode={ORIG}&destinationAirportCode={DEST}

Additional headers:
  x-referrer: fs

Response:

{
  "relevantList": "First",
  "footer": [
    "If your upgrade has cleared or you clear the waitlist, please refresh your mobile boarding pass.",
    "The order of names may change as additional customers check in."
  ],
  "waitList": [
    {
      "listName": "First",
      "seatsAvailableLabel": "Available seats",
      "seatsAvailableValue": 1,
      "seatsAvailableSemanticColor": "failure",
      "passengers": [
        {"order": 1, "displayName": "BRI, K", "cleared": false, "seat": null, "highlighted": false},
        {"order": 2, "displayName": "MAT, R", "cleared": false, "seat": null, "highlighted": false}
      ]
    },
    {
      "listName": "Standby",
      "seatsAvailableLabel": "Available seats",
      "seatsAvailableValue": 45,
      "seatsAvailableSemanticColor": "success",
      "passengers": [
        {"order": 1, "displayName": "MIT, R", "cleared": false, "seat": null, "highlighted": false},
        {"order": 2, "displayName": "MAR, M", "cleared": false, "seat": null, "highlighted": false}
      ]
    }
  ]
}

Field Reference

Field Description
waitList[].listName "First", "Standby", etc.
waitList[].seatsAvailableValue Number of open seats for that class
waitList[].seatsAvailableSemanticColor "success" (green, many), "warning" (yellow, few), "failure" (red, <=1)
waitList[].passengers[].displayName Passenger name (LAST, F)
waitList[].passengers[].order Position on list (1-based)
waitList[].passengers[].cleared true if cleared from list
waitList[].passengers[].seat Seat number if cleared

SPIRIT AIRLINES

Base Setup

Domain: https://api.spirit.com
Auth: APIM subscription key (no login required)
Method: Plain HTTP (curl/fetch) — no Playwright needed

Step 1: Get Flight Status

POST https://api.spirit.com/customermobileprod/2.8.0/v3/GetFlightInfoBI

Headers:
  Content-Type: application/json
  Ocp-Apim-Subscription-Key: c6567af50d544dfbb3bc5dd99c6bb177
  Platform: Android

Body:
{
  "departureStation": "FLL",
  "arrivalStation": "ATL",
  "departureDate": "2026-04-08"
}

Response:

{
  "getFlightInfoBIResult": [
    {
      "flightNumber": "NK204",
      "journeyID": 1,
      "departureStationCode": "FLL",
      "arrivalStationCode": "ATL",
      "departureGate": "F4",
      "arrivalGate": "C4",
      "departureTerminal": "4",
      "arrivalTerminal": "N",
      "legStatus": "Arrived",
      "departureTime": "8:57am",
      "arrivalTime": "11:04am",
      "scheduledDeparture": "scheduled at 8:24am",
      "scheduledArrival": "scheduled at 10:22am",
      "departureCity": "Fort Lauderdale, FL",
      "arrivalCity": "Atlanta, GA",
      "flightStatusColor": "#FF9500",
      "totalDurationMinutes": 127,
      "departureDateTime": "2026-04-08T08:57:00",
      "arrivalDateTime": "2026-04-08T11:04:00"
    }
  ]
}

Step 2: Get Station/Route Network

GET https://api.spirit.com/customermobileprod/2.8.0/v1/stations

Headers:
  Ocp-Apim-Subscription-Key: c6567af50d544dfbb3bc5dd99c6bb177

Returns all Spirit stations with airport codes, coordinates, city names, and which routes connect to each station.

Notes

  • Spirit is a ULCC — no standby lists or upgrade waitlists
  • Seat-level availability requires a booking session + Akamai bypass (not publicly accessible)
  • The APIM key was decrypted from the native library in the Android APK

KOREAN AIR

Base Setup

Domain: https://www.koreanair.com
Auth: None required for flight status; minimal headers
Method: Playwright → navigate to koreanair.com → page.evaluate(fetch)

Step 1: Search Flights

POST /api/fs/scheduleFlightSearch/flight/status/app

Headers:
  Content-Type: application/json
  Accept: application/json
  channel: app

Body (by flight number):
{
  "departureDate": "20260408",
  "departureLocationCode": "",
  "arrivalLocationCode": "",
  "flightNumber": "017",
  "searchOption": "FLTNUM"
}

Body (by route):
{
  "departureDate": "20260408",
  "departureLocationCode": "ICN",
  "arrivalLocationCode": "LAX",
  "flightNumber": "",
  "searchOption": "ROUTE"
}

Response:

{
  "scheduleInformation": {
    "flightInformation": {
      "flightDetailsList": [
        {
          "departureAirport": "ICN",
          "arrivalAirport": "LAX",
          "departureDate": "20260408",
          "departureTime": "1430",
          "arrivalTime": "0940",
          "flightNumber": "017",
          "flyingTime": "1110",
          "status": {"code": "ARV", "codeByUI": "ARV"},
          "cabinClassInfoList": [
            {"cabinClassOfService": "1"},
            {"cabinClassOfService": "2"},
            {"cabinClassOfService": "3"}
          ],
          "scheduleFlightUIInfoMsOutVo": {
            "flightStatus": "arrived",
            "departureUIInfo": {
              "scheduledTime": "14:30",
              "actualTime": "14:27"
            },
            "arrivalUIInfo": {
              "scheduledTime": "09:40",
              "actualTime": "09:47"
            }
          }
        }
      ]
    }
  }
}

Step 2: Get Seat Count

POST /api/et/ibeSupport/flightSeatCount

Headers:
  Content-Type: application/json
  Accept: application/json
  channel: pc

Body:
{
  "carrierCode": "KE",
  "flightNumber": "017",
  "departureAirport": "ICN",
  "arrivalAirport": "LAX",
  "departureDate": "20260409"
}

Response:

{
  "seatCount": 0,
  "carrierCode": "KE",
  "flightNumber": "017"
}

Note: seatCount returns available seats. Returns 0 for dates too far out — works best within 24-48 hours of departure.

Step 3: Get Availability

POST /api/fs/scheduleFlightSearch/sdcAirMultiAvailability

Headers:
  Content-Type: application/json
  Accept: application/json
  channel: pc

Body:
{
  "departureDate": "20260409",
  "departureLocationCode": "ICN",
  "arrivalLocationCode": "LAX",
  "flightNumber": "001",
  "searchOption": "FLTNUM"
}

Playwright Integration Pattern

All airlines except Spirit use the same Playwright wrapper pattern:

const { chromium } = require('playwright');

async function queryAirline(airlineDomain, setupHeaders, apiCall) {
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext();
  
  if (setupHeaders) {
    await context.setExtraHTTPHeaders(setupHeaders);
  }
  
  const page = await context.newPage();
  await page.goto(airlineDomain);
  await page.waitForTimeout(5000);
  
  const result = await page.evaluate(apiCall);
  
  await browser.close();
  return result;
}

// UNITED example:
const unitedLoads = await queryAirline(
  'https://www.united.com/en/us/flightstatus',
  null,
  async () => {
    const tokenResp = await fetch('/api/auth/anonymous-token');
    const { data } = await tokenResp.json();
    const token = data.token.hash;
    
    const resp = await fetch(
      '/api/flightstatus/upgradeListExtended?flightNumber=2238&flightDate=2026-04-08&fromAirportCode=EWR',
      { headers: { 'Accept': 'application/json', 'x-authorization-api': 'bearer ' + token } }
    );
    return resp.json();
  }
);

// AMERICAN example:
const aaLoads = await queryAirline(
  'https://cdn.flyaa.aa.com/apiv2/mobile-flifo/flightSchedules/v1.0?origin=DFW&destination=IAH&departureDay=9&departureMonth=4&searchType=schedule&noOfFlightsToDisplay=20',
  {
    'User-Agent': 'Android/2025.31 Pixel 7|14|1080|2400|1.0|AmericanAirlines',
    'x-clientid': 'MOBILE',
    'Accept': 'application/json',
    'Device-ID': 'device-001'
  },
  async () => {
    const resp = await fetch(
      'https://cdn.flyaa.aa.com/api/mobile/loyalty/waitlist/v1.2?carrierCode=AA&flightNumber=2209&departureDate=2026-04-08&originAirportCode=DFW&destinationAirportCode=IAH',
      { headers: { 'Accept': 'application/json', 'x-referrer': 'fs' } }
    );
    return resp.json();
  }
);

// KOREAN AIR example:
const keStatus = await queryAirline(
  'https://www.koreanair.com',
  null,
  async () => {
    const resp = await fetch('/api/fs/scheduleFlightSearch/flight/status/app', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'channel': 'app' },
      body: JSON.stringify({
        departureDate: '20260408',
        flightNumber: '017',
        searchOption: 'FLTNUM',
        departureLocationCode: '',
        arrivalLocationCode: ''
      })
    });
    return resp.json();
  }
);

Spirit (plain HTTP, no Playwright):

const spiritStatus = await fetch('https://api.spirit.com/customermobileprod/2.8.0/v3/GetFlightInfoBI', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Ocp-Apim-Subscription-Key': 'c6567af50d544dfbb3bc5dd99c6bb177',
    'Platform': 'Android'
  },
  body: JSON.stringify({
    departureStation: 'FLL',
    arrivalStation: 'ATL',
    departureDate: '2026-04-08'
  })
}).then(r => r.json());

EMIRATES

Base Setup

Domain: https://www.emirates.com
Auth: None required for flight status
Method: Plain HTTP (curl/fetch) — no Playwright needed

Step 1: Flight Status

GET https://www.emirates.com/service/flight-status?departureDate={YYYY-MM-DD}&flight={flightNumber}

Headers: none required

Response:

{
  "results": [{
    "airlineDesignator": "EK",
    "flightNumber": "0221",
    "flightId": "2026040700221DXB",
    "flightDate": "2026-04-07",
    "flightRoute": [{
      "legNumber": "1",
      "originActualAirportCode": "DXB",
      "destinationActualAirportCode": "DFW",
      "originPlannedAirportCode": "DXB",
      "destinationPlannedAirportCode": "DFW",
      "statusCode": "ARVD",
      "flightPosition": 100,
      "totalTravelDuration": "17:30",
      "isIrregular": "false",
      "departureTime": {
        "schedule": "2026-04-08T01:10:00Z",
        "estimated": "2026-04-08T01:15:00Z",
        "actual": "2026-04-08T01:12:00Z"
      },
      "arrivalTime": {
        "schedule": "2026-04-08T09:40:00Z",
        "estimated": "2026-04-08T09:34:00Z",
        "actual": "2026-04-08T09:32:00Z"
      },
      "departureTerminal": "Terminal 3",
      "arrivalTerminal": "Terminal D",
      "flightOutageType": 0
    }]
  }],
  "metaLinks": []
}

Step 2: Flight Load / Staff Standby (requires PNR)

Mobile API base: https://mobileapp.emirates.com/

GET /olci/v1/checkin/staffinformation/{pnr}/{lastName}

Returns FlightLoadResponse with:

  • isStaffSubLoadTableAvl - whether subload table is available
  • staffPax.passengers[] - staff passenger list with check-in status
  • flights[] - per-flight load data
  • Per passenger: currentPriority, totalPriority, status, flightNumber

Notes

  • Flight status works from plain curl with zero auth — simplest of all airlines
  • Staff standby/flight load data requires PNR + last name (mobile app only)
  • The app has a full staff travel system: standby priority tracking, class downgrade acceptance, subload questionnaires
  • Internal backend leaked in response: business-services-cache-bex-prod.dub.prd01.digitalattract.aws.emirates.prd

ALASKA AIRLINES

Base Setup

Domain: https://www.alaskaair.com
Mobile API: /1/guestservices/customermobile/
Auth: Requires booking confirmation code for load data
Method: Playwright (website uses shadow DOM web components)

Step 1: Flight Status

GET /1/guestservices/customermobile/flights/status/{airlineCode}/{flightNumber}/{departureDate}

Example: /1/guestservices/customermobile/flights/status/AS/1084/2026-04-08

Step 2: Flight Status v2 (by route)

POST /1/guestservices/customermobile/mobileservices/reservation/flights/status

Body:
{
  "airlineCode": "AS",
  "flightNumber": "1084",
  "departureDate": "2026-04-08",
  "departureCityCode": "SEA",
  "arrivalCityCode": "LAX"
}

Response includes showPriorityList boolean indicating if standby list is available for that flight.

Step 3: Seat Availability / Remaining Seats (requires confirmation code)

POST /1/guestservices/customermobile/seats/SeatUpgradesByCabinRec/{confirmationCode}

Body:
{
  "adobeMarketingCloudVisitorID": "{visitor_id}"
}

Response:

[
  {
    "flightNumber": 1084,
    "origin": "SEA",
    "destination": "LAX",
    "cabinType": "First",
    "remainingSeats": 4,
    "upgradePrice": 149.00,
    "equipment": "Boeing 737-900ER"
  }
]

Notes

  • Flight status is public, but remainingSeats data requires a valid confirmation code
  • Mobile API paths start with /1/guestservices/customermobile/
  • Uses Ktor HTTP client (modern Kotlin)
  • Website uses deep shadow DOM -- Playwright automation is complex

JETBLUE

Base Setup

API Domain: https://az-api.jetblue.com
Auth: API key (no login required)
Method: Plain HTTP (curl/fetch) — no Playwright needed
API Key: 49fc015f1ba44abf892d2b8961612378

Step 1: Flight Status by Number

GET https://az-api.jetblue.com/flight-status/get-by-number?number={flightNumber}&date={YYYY-MM-DD}

Headers:
  apikey: 49fc015f1ba44abf892d2b8961612378
  Accept: application/json

Response:

{
  "flights": [{
    "tripOrigin": "LAX",
    "tripDestination": "JFK",
    "isConnecting": false,
    "isThroughFlight": false,
    "legs": [{
      "flightNo": "524",
      "flightStatus": "IN FLIGHT",
      "flightStatusGroup": "standardPostDeparture",
      "originAirport": "LAX",
      "originGate": "16",
      "originTerminal": "1",
      "actualDeparture": "2026-04-08T13:19:00-07:00",
      "scheduledDeparture": "2026-04-08T13:27:00-07:00",
      "doorCloseTime": "2026-04-08T13:12:00-07:00",
      "boardingTime": "2026-04-08T12:42:00-07:00",
      "destinationAirport": "JFK",
      "destinationGate": "518",
      "destinationTerminal": "5",
      "actualArrival": "2026-04-08T21:37:00-04:00",
      "scheduledArrival": "2026-04-08T21:55:00-04:00",
      "baggageClaim": "4",
      "equipmentType": "3NL",
      "tailNumber": "4074"
    }]
  }]
}

Step 2: Priority List (Standby/Upgrade - requires check-in session)

The app has retrievePriorityList which returns PriorityListPassenger:

{
  "shortLastName": "DOE",
  "shortFirstName": "J",
  "code": "SA",
  "order": 1,
  "hasSeat": false
}

This requires an active check-in session (Cookie header). Accessible during check-in flow only.

Step 3: Crystal Blue Seat Map

POST https://az-api.jetblue.com/mobile_seatmap
Headers:
  Ocp-Apim-Subscription-Key: a5ee654e981b4577a58264fed9b1669c
  Content-Type: application/json

Notes

  • Flight status works from plain curl with just the API key
  • Priority list requires check-in session
  • Second APIM key a5ee654e981b4577a58264fed9b1669c used for seat map and logging

FRONTIER AIRLINES

APK not available for download from any third-party source. Frontier is a ULCC like Spirit -- minimal standby/upgrade features expected. Not yet analyzed.


Rate Limits & Best Practices

  • United: Token expires in ~30min. Cache and refresh. No known rate limit.
  • American: No token needed. Akamai may throttle if too many requests from same browser session. Rotate browser contexts.
  • Spirit: APIM key is shared across all app users. No known rate limit but don't abuse.
  • Korean Air: No auth needed for status endpoints. channel header is required.
  • All Playwright airlines: Reuse browser context across multiple queries to avoid re-establishing sessions. Close and recreate if you get 403s.