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>
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:
- Navigate to the airline's domain to establish a browser session
- 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 availablestaffPax.passengers[]- staff passenger list with check-in statusflights[]- 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
remainingSeatsdata 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
a5ee654e981b4577a58264fed9b1669cused 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.
channelheader is required. - All Playwright airlines: Reuse browser context across multiple queries to avoid re-establishing sessions. Close and recreate if you get 403s.