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

793 lines
21 KiB
Markdown

# 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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"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:
```javascript
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):
```javascript
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:**
```json
{
"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:**
```json
[
{
"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:**
```json
{
"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`:
```json
{
"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.