# Airlines API Reverse Engineering — Complete Findings

Everything I know from reverse-engineering 9 airline mobile apps (Android APKs and one decrypted iOS IPA) for flight loads, standby lists, and flight status data. Last updated 2026-04-11.

---

## TL;DR — Status by airline

| Airline | Flight status | Loads / standby | Auth |
|---|---|---|---|
| **United Airlines** | yes (curl-able via Playwright) | **yes — full per-cabin loads + standby list** | anonymous token |
| **American Airlines** | yes | **yes — waitlist + seats per class** | none (mobile UA only) |
| **Spirit** | yes (curl direct) | no (ULCC, no standby) | static APIM key |
| **JetBlue** | yes (curl direct) | yes via PNR | API key + check-in session |
| **Korean Air** | yes | partial (`flightSeatCount` returns 0 for far-out dates) | none |
| **Emirates** | yes (curl direct) | staff travel only via PNR | none for status |
| **Delta** | yes (mobile API curl-able with mobile UA) | **no public path** — all gated by PNR or auth | mixed |
| **Alaska** | yes | requires confirmation code | mobile API |
| **Frontier** | not analyzed | — | — |
| **British Airways** | not deeply analyzed | — | — |

**Bottom line on loads:** United and American expose true seat counts and standby lists with no PNR. Everyone else either requires a PNR, an authenticated session, or doesn't have the data at all.

---

## Architecture overview

### Three access tiers across all airlines

1. **Plain HTTP (curl-able):** Spirit, JetBlue, Emirates, Korean Air, Delta `flight-status-mobile/details`
2. **Playwright with mobile User-Agent:** American (`cdn.flyaa.aa.com`), Delta `flight-status/details` (web variant)
3. **Playwright with full browser session + Akamai cookies:** United (`united.com`), Delta `offers/shop` (still gated by auth on top)

### Anti-bot protection by airline

| Airline | WAF/CDN | Bot detection | Bypass strategy |
|---|---|---|---|
| United | Akamai | Akamai BMP | Playwright session token from page load |
| American | Akamai | Akamai ACF sensor | Mobile UA `Android/2025.31 Pixel 7|14|...|AmericanAirlines` passes validation |
| Delta | Dynatrace + Akamai BMP | `AkamaiBMP.framework` (confirmed in iOS bundle) | Mobile UA `FlyDelta/24.10.1 (Pixel 7; Android 14; Build/UQ1A.240205.004)` for `mobile-api`; shop endpoints fail without sensor data |
| Spirit | Akamai | CyberFend BMP | Static APIM key works for GET; POST blocked without sensor data |
| JetBlue | Azure APIM | Static API key | Just send `apikey` header |
| Korean Air | minimal | minimal | `channel: app` header |
| Emirates | none on flight-status | — | Just curl |
| Alaska | minimal | minimal | mobile API path `/1/guestservices/customermobile/` |

---

## 1. UNITED AIRLINES — fully working

**Domain:** `https://www.united.com`
**Auth:** anonymous token (~30 min lifetime)
**Method:** Playwright → navigate to united.com → `page.evaluate(fetch())`

### Step 1 — get token
```
GET /api/auth/anonymous-token
```
Response:
```json
{ "data": { "token": { "hash": "DAAAA...", "expiresAt": "..." } } }
```

### Step 2 — flight loads (the prize)
```
GET /api/flightstatus/upgradeListExtended?flightNumber={NUM}&flightDate={YYYY-MM-DD}&fromAirportCode={ORIGIN}
Headers:
  x-authorization-api: bearer {token.hash}
  Accept: application/json
```

Response (full per-cabin loads + standby + cleared upgrades):
```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, "booked": 16, ... },
    { "cabin": "Rear", "capacity": 202, "booked": 164, "revenueStandby": 2, "sa": 4 }
  ],
  "checkInSummaries": [
    { "cabin": "Front", "capacity": 50, "total": 50,
      "etktPassengersCheckedIn": 50, "revStandbyCheckedInWithoutSeats": 0,
      "nonRevStandbyCheckedInWithoutSeats": 0, "children": 0, "infants": 0, "bags": 0 }
  ],
  "front": {
    "cleared": [
      { "currentCabin": "Front", "bookedCabin": "Rear",
        "firstName": "T", "lastName": "JEN", "passengerName": "T/JEN",
        "seatNumber": "1G", "clearanceType": "Upgrade", "skipped": false }
    ],
    "standby": []
  }
}
```

### Field reference
| Field | Meaning |
|---|---|
| `pbts[].cabin` | "Front" (Polaris/First), "Middle" (Premium Plus), "Rear" (Economy) |
| `pbts[].capacity` | Total cabin seats |
| `pbts[].booked` | Sold/assigned |
| `pbts[].revenueStandby` | Revenue standby pax |
| `pbts[].sa` | Space available (non-rev standby) |
| `pbts[].ps` | Positive space |
| `pbts[].waitList` | Waitlisted |
| `front.cleared[]` | Cleared upgrades with name + seat |
| `front.standby[]` | Standby list ordered |

Derived: `availableSeats = capacity - booked`, `loadFactor = booked / capacity`.

### Other working United endpoints
- `GET /api/flightstatus/status/{NUM}/{DATE}/{ORIGIN}/{DEST}?carrierCode=UA&useLegDestDate=true` — gates, times, delays, aircraft type, tail
- `GET /api/flightstatus/seatmap/{NUM}/{DATE}/{ORIGIN}/{DEST}?carrierCode=UA` — interactive seat map
- `POST /api/flight/FetchSSENestedFlights` — full flight search (SSE stream) returning 40 itineraries with pricing per cabin and booking class codes (RBDs). Booking class only available = nearly full.
- `POST /api/FlexPricer/CalendarPricing` — low-fare calendar across dates

### Mobile API (legacy, less reliable)
Base URLs (loaded from runtime URL catalog):
- Production: `https://smartphone.united.com/UnitedMobileDataServices/api`
- Preview: `https://smartphone-preview.united.com/UnitedMobileDataServices/api`

Endpoints (all require MileagePlus session):
- `POST /Shopping/Shop` — main flight search
- `POST /SeatEngine/PreviewSeatMap` — seat map with availability
- `GET /StandByList/GetStandbyList` — standby list (load data)
- `GET /UpgradeList/GetUpgradeList` — upgrade waitlist
- `POST /Shopping/GetONTimePerformence` — on-time performance
- `POST /EmployeeReservation/GetFlightAvailability` — employee flight availability

Note: smartphone.united.com times out from non-mobile networks. Web API via Playwright is the working path.

### Quirks
- Token expires in ~30 min — cache and refresh
- Token bound to browser session, not portable to curl (TLS fingerprinting)
- `x-authorization-api: bearer ...` not `Authorization`
- Token starts with `DAAAA...`

---

## 2. AMERICAN AIRLINES — fully working

**Domain:** `https://cdn.flyaa.aa.com`
**Auth:** none (mobile UA only)
**Method:** Playwright with mobile UA headers → navigate → `page.evaluate(fetch())`

### Required headers (set on context)
```
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}
```

**Critical:** the User-Agent format is `Android/{version} {device}|{osVersion}|{width}|{height}|1.0|AmericanAirlines`. Without this exact format, Akamai returns `{"error":["Invalid user-agent header"]}`.

### Step 1 — search flights by route
```
GET /apiv2/mobile-flifo/flightSchedules/v1.0
  ?origin={ORIGIN}
  &destination={DEST}
  &departureDay={DAY}
  &departureMonth={MONTH}
  &searchType=schedule
  &noOfFlightsToDisplay=20
```
Returns flight list with flight numbers, times, carrier info, `showUpgradeStandbyList`, `allowFSN`.

### Step 2 — waitlist + available seats (the prize)
```
GET /api/mobile/loyalty/waitlist/v1.2
  ?carrierCode=AA
  &flightNumber={NUM}
  &departureDate={YYYY-MM-DD}
  &originAirportCode={ORIGIN}
  &destinationAirportCode={DEST}
Headers:
  x-referrer: fs
```
Response:
```json
{
  "relevantList": "First",
  "waitList": [
    {
      "listName": "First",
      "seatsAvailableLabel": "Available seats",
      "seatsAvailableValue": 1,
      "seatsAvailableSemanticColor": "failure",
      "passengers": [
        { "order": 1, "displayName": "BRI, K", "cleared": false, "seat": null },
        { "order": 2, "displayName": "MAT, R", "cleared": false }
      ]
    },
    {
      "listName": "Standby",
      "seatsAvailableValue": 45,
      "seatsAvailableSemanticColor": "success",
      "passengers": [...]
    }
  ]
}
```

### Field reference
| Field | Meaning |
|---|---|
| `waitList[].listName` | "First", "Standby", etc. |
| `waitList[].seatsAvailableValue` | Open seats for that class |
| `waitList[].seatsAvailableSemanticColor` | "success" (many), "warning" (few), "failure" (≤1) |
| `passengers[].displayName` | "LAST, F" |
| `passengers[].order` | 1-based position |
| `passengers[].cleared` | true if cleared |
| `passengers[].seat` | Seat if cleared |

### Other AA endpoints (all require mobile UA)
| Method | Path | Description |
|---|---|---|
| POST | `/apiv2/mobile-booking/search/itinerary/v2.0` | Flight search itinerary |
| POST | `/apiv2/mobile-booking/search/summary/v2.0` | Flight search summary |
| POST | `/apiv2/mobile-booking/search/weekly/v2.0` | Weekly fare calendar |
| GET | `/api/mobile-fly/flybff/flightstatus` | Flight status |
| GET | `/apiv2/mobile-flifo/flightstatus/v1.0` | Flight status (FLIFO) |
| GET | `/api/mobilemanage/manage_v1/seatInventory` | Seat inventory |
| POST | `/apiv2/mobile-ancillary-bff/seats/availability/v1.0` | Seat availability |
| GET | `/apiv2/mobile-manage/seatsLayout/v1.0/aircraftConfig` | Aircraft seat layouts |

### Auth-gated endpoints
- OAuth2 via `login.aa.com/loyalty/as/token.oauth2`
- Booking endpoints require `x-acf-sensor-data` (Akamai bot protection sensor) — 403 without it
- 90s connect timeout, 180s read timeout

### Other base URLs (env-specific)
| Env | URL |
|---|---|
| Production | `https://cdn.flyaa.aa.com` |
| QA | `https://cdn.kqa1.flyaa.aa.com` |
| Stage | `https://cdn.kiqa.flyaa.aa.com` |
| SSO | `https://login.aa.com` |

### Quirks
- The `/api/mobile/loyalty/waitlist/v1.2` endpoint is the waitlist data — note `loyalty` in the path
- Add `x-referrer: fs` header on the waitlist call (fs = flight status)
- Direct curl gets blocked by TLS fingerprinting; must use Playwright
- The Android APK confirmed the User-Agent format passes Akamai's validation

---

## 3. SPIRIT AIRLINES — partial (status only)

**Domain:** `https://api.spirit.com`
**Auth:** Static APIM key (decrypted from native lib)
**Method:** Plain curl/HTTP — no browser needed

### Production base URLs
| Env | URL |
|---|---|
| Production | `https://api.spirit.com/customermobileprod/2.8.0/` |
| Stage | `https://api.spirit.com/stage-customermobileapi/` |
| UAT | `https://apiqa.spirit.com/qa01-customermobileapi/` |
| CMS | `https://content.spirit.com/api/content/` |

### Required header
```
Ocp-Apim-Subscription-Key: c6567af50d544dfbb3bc5dd99c6bb177
```

### Working endpoints

#### Flight status (no token)
```
POST /v3/GetFlightInfoBI
Content-Type: application/json
Ocp-Apim-Subscription-Key: c6567af50d544dfbb3bc5dd99c6bb177
Platform: Android

{
  "departureStation": "FLL",
  "arrivalStation": "ATL",
  "departureDate": "2026-04-08"
}
```
Response includes: flight number, gates, terminals, status, times, scheduled vs actual.

#### Stations / route network (no token)
```
GET /v1/stations
Ocp-Apim-Subscription-Key: c6567af50d544dfbb3bc5dd99c6bb177
```
Returns all Spirit stations with coordinates, markets, route connections.

#### Dynamic content (no token)
```
GET /v1/getdynamiccontent
Ocp-Apim-Subscription-Key: c6567af50d544dfbb3bc5dd99c6bb177
```

#### Anonymous token (needs Akamai bypass)
```
POST /v2/Token
Ocp-Apim-Subscription-Key: c6567af50d544dfbb3bc5dd99c6bb177
{}
```
Returns JWT token (~15 min lifetime). Required for most authenticated endpoints. **Blocked by Akamai BMP from curl/Playwright** — needs CyberFend sensor data.

### Auth-gated endpoints (need JWT + Akamai bypass)
| Method | Path | Description |
|---|---|---|
| POST | `/v5/Flight/Search` | Flight search with fares by cabin |
| POST | `/v1/booking/seatmap` | Seat map |
| GET | `/v3/booking/seatmaps/journey/{journeyKey}` | Journey seat map |
| POST | `/v1/calendar/availabledates` | Available dates calendar |
| POST | `/v1/booking/flightdetails` | Flight details |
| GET | `/v3/bundle/ssrs` | Bundle availability |
| GET | `/v1/bundle/UpsellAvailability` | Upsell availability |
| POST | `/v3/mytrips` | My trips |
| GET | `/v1/OnD/Countries` | Countries (only needs APIM key) |

### Decrypted APIM keys
| Environment | Encrypted | Decrypted |
|---|---|---|
| Production | `bD+Vg9tjmK4h19uwfGdl...` | **`c6567af50d544dfbb3bc5dd99c6bb177`** |
| Dev/UAT | `G123dLr8BDvwfr8fkHds...` | `81ffe172c5c741cdac0a2cc13ab19b54` |
| Stage | `Jh7I2ohTGkMoaQ9VDcu8...` | `daa76fa3c25d423f880b939d56992553` |

### Encryption details
- Algorithm: AES/CBC/PKCS5Padding
- Key derivation: SHA-512("1983Miramar"), first 16 bytes
- IV: `Aw@#EDfTGec3Rtd!`
- Native library: `libspirit-lib.so`

### Notes
- Spirit is a ULCC — no standby lists or upgrade waitlists exist in the system
- Seat-level data is in the seat map, but seat map needs auth + Akamai bypass
- POST endpoints blocked by Akamai BMP from curl/Playwright (needs CyberFend sensor data from real device)
- A real Android device or Frida on a rooted device can bypass CyberFend

---

## 4. JETBLUE — partial (status works, loads need PNR)

**API domain:** `https://az-api.jetblue.com`
**Check-in domain:** `https://mobilecheckin.jetblue.com/checkin/`
**Auth:** API key (no login)
**Method:** Plain curl/HTTP

### Static API keys (extracted)
- Main: **`49fc015f1ba44abf892d2b8961612378`**
- Seat map / logging: `a5ee654e981b4577a58264fed9b1669c`

### Step 1 — flight status by number (curl works)
```
GET https://az-api.jetblue.com/flight-status/get-by-number?number={NUM}&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": "...", "scheduledDeparture": "...",
      "doorCloseTime": "...", "boardingTime": "...",
      "destinationAirport": "JFK", "destinationGate": "518", "destinationTerminal": "5",
      "actualArrival": "...", "scheduledArrival": "...",
      "baggageClaim": "4", "equipmentType": "3NL", "tailNumber": "4074"
    }]
  }]
}
```

### Step 2 — priority list / loads (REQUIRES PNR)
```
Flow:
  POST registerClient                  → returns session cookie
  POST identifyPNR (with confirmation) → validates PNR + name
  POST retrievePriorityList            → returns full load + standby data
```

`RetrievePriorityListResponse`:
```json
{
  "numberOfCapacityJ": 16,
  "numberOfCapacityY": 144,
  "numberOfAvailableSeatsJ": 3,
  "numberOfAvailableSeatsY": 22,
  "numberOfConfirmedPassengersJ": 13,
  "numberOfConfirmedPassengersY": 120,
  "numberOfAuthorizedSeatsJ": 16,
  "numberOfAuthorizedSeatsY": 144,
  "numberOfAuthorizedSeatsTotal": 160,
  "numberOfConfirmedPassengersTotal": 133,
  "numberOfStandbyPassengers": 4,
  "numberOfWaitListedPassengers": 2,
  "priorityListPassengers": [
    { "shortLastName": "DOE", "shortFirstName": "J", "code": "SA", "order": 1, "hasSeat": false },
    { "shortLastName": "SMI", "shortFirstName": "A", "code": "SA", "order": 2, "hasSeat": false }
  ],
  "flight": { ... }
}
```

This is JetBlue's equivalent of United's `upgradeListExtended` — full capacity/booked/available per cabin (J = Mint, Y = Core), plus the standby passenger list.

### Step 3 — seat map
```
POST https://az-api.jetblue.com/mobile_seatmap
Headers:
  Ocp-Apim-Subscription-Key: a5ee654e981b4577a58264fed9b1669c
  Content-Type: application/json
```

### Notes
- `registerClient` works with just an airport code; the gate is `identifyPNR` which validates PNR
- API keys captured from web traffic via Playwright
- No Akamai involvement on the API endpoints — this is Azure APIM with key-based auth

---

## 5. KOREAN AIR — partial

**Domain:** `https://www.koreanair.com`
**Auth:** none for flight status
**Method:** Playwright → navigate → `page.evaluate(fetch())`

### Step 1 — flight search (by number or route)
```
POST /api/fs/scheduleFlightSearch/flight/status/app
Content-Type: application/json
Accept: application/json
channel: app

# By flight number
{
  "departureDate": "20260408",
  "departureLocationCode": "",
  "arrivalLocationCode": "",
  "flightNumber": "017",
  "searchOption": "FLTNUM"
}

# By route
{
  "departureDate": "20260408",
  "departureLocationCode": "ICN",
  "arrivalLocationCode": "LAX",
  "flightNumber": "",
  "searchOption": "ROUTE"
}
```
Response: flight detail list with departure/arrival times, status, cabin classes available.

### Step 2 — seat count (often returns 0)
```
POST /api/et/ibeSupport/flightSeatCount
Content-Type: application/json
channel: pc

{
  "carrierCode": "KE",
  "flightNumber": "017",
  "departureAirport": "ICN",
  "arrivalAirport": "LAX",
  "departureDate": "20260409"
}
```
Response: `{ "seatCount": 0, "carrierCode": "KE", "flightNumber": "017" }`

`seatCount` is the number of available seats. **Returns 0 for far-out dates** — works best within 24-48 hours of departure. May need to call `removeGarbageSession` first to establish IBE session.

### Step 3 — full availability
```
POST /api/fs/scheduleFlightSearch/sdcAirMultiAvailability
Content-Type: application/json
channel: pc

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

### Notes
- `channel` header required: `app` for flight search, `pc` for seat count
- `flightSeatCount` is real but only useful close to departure
- Try with `departureDate` as int to test date-too-far behavior

---

## 6. EMIRATES — flight status only

**Domain:** `https://www.emirates.com`
**Mobile API:** `https://mobileapp.emirates.com/`
**Auth:** none for flight status
**Method:** Plain curl

### Flight status (curl works)
```
GET https://www.emirates.com/service/flight-status?departureDate={YYYY-MM-DD}&flight={NUM}
```
Response:
```json
{
  "results": [{
    "airlineDesignator": "EK",
    "flightNumber": "0221",
    "flightId": "2026040700221DXB",
    "flightDate": "2026-04-07",
    "flightRoute": [{
      "legNumber": "1",
      "originActualAirportCode": "DXB",
      "destinationActualAirportCode": "DFW",
      "statusCode": "ARVD",
      "flightPosition": 100,
      "totalTravelDuration": "17:30",
      "departureTime": { "schedule": "...", "estimated": "...", "actual": "..." },
      "arrivalTime": { "schedule": "...", "estimated": "...", "actual": "..." },
      "departureTerminal": "Terminal 3",
      "arrivalTerminal": "Terminal D"
    }]
  }]
}
```

### Staff travel / flight load (requires PNR)
```
GET https://mobileapp.emirates.com/olci/v1/checkin/staffinformation/{PNR}/{LASTNAME}
```
Returns `FlightLoadResponse`:
- `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 is the simplest of all airlines — pure curl, zero auth, zero headers
- Staff standby/flight load data is for staff travel only and requires PNR + last name
- Internal backend leaked in response: `business-services-cache-bex-prod.dub.prd01.digitalattract.aws.emirates.prd`
- Mobile app has full staff travel system: standby priority tracking, class downgrade acceptance, subload questionnaires

---

## 7. DELTA — extensive deep dive (this session)

**Status:** flight status works without PNR. **Loads do NOT have a public path** — every endpoint that returns seat/standby data requires either a SkyMiles login or a valid PNR + name on the flight.

### Working endpoint — flight-status-mobile/details (no PNR, no auth)

```
POST https://mobile-api.delta.com/flight-status-mobile/details
Content-Type: application/json
Accept: application/json
User-Agent: FlyDelta/24.10.1 (Pixel 7; Android 14; Build/UQ1A.240205.004)

{
  "airlineCode": "DL",
  "flightNumber": "996",
  "flightOriginDate": "2026-04-12"
}
```

**3 fields. That's the entire body.** The APK class `m3/a.java` (FlightStatusAirlineRequest) defines this. Domain `aws_mobile_public` (no auth interceptor). Endpoint is on the network whitelist `u3/d.java`.

**Critical:** The User-Agent must look like the Delta mobile app. Without it: 444 from Akamai. With it: 200 OK with full response.

Returns (~26KB JSON):
- Flight number, ship number (tail), aircraft code/name (e.g. "Airbus A321neo"), industry code
- Departure: gate, terminal, scheduled/estimated/actual times (local + GMT), ICAO code, station coords
- Arrival: same fields
- Performance stats (`a15InTimeStatPct`, `a30DelayedStatPct`, `cancellationStatPct`)
- Inbound flight info (the flight bringing the aircraft in)
- `productsByBrands[]` — for each cabin (BE/MAIN/DCP/FIRST), the amenities (Wi-Fi, USB, Studio, etc.) and meals

**What it does NOT include:** `seatsRemaining`, `standbyPriorityInfo`, `upgradeStandbyModel`, any seat counts. Verified by reading the entire response and the `DatedOperatingLeg.java` model which has zero seat fields.

### Working endpoint — flight-status/schedules (route lookup, no PNR)

```
POST https://flightinformation-api.delta.com/flight-status/schedules
Content-Type: application/json
Accept: application/json
User-Agent: FlyDelta/24.10.1 (Pixel 7; Android 14; Build/UQ1A.240205.004)

{
  "flightSegments": [{
    "flightOriginDate": "2026-04-12",
    "originAirportCode": "ATL",
    "destinationAirportCode": "LAX",
    "airlineCode": "DL"
  }],
  "filterOptions": {
    "schedule": {
      "directOnlyIndicator": false,
      "includeElapsedOrAccumulatedTime": true,
      "flightStops": [],
      "departureTimeRanges": [],
      "excludeOtherAirlineInd": true,
      "connections": [{ "connectionPointNum": "", "connectionCity": "", "minConnectionTime": "" }],
      "startRecord": 1,
      "endRecord": 30
    },
    "availability": { "indicator": false, "seatTypeCode": "A", "onlyRevenueClassInd": false }
  }
}
```

Returns trip list for the route: every flight number, aircraft code, scheduled vs estimated times, status. Confirmed 10 nonstop ATL→LAX trips for a future date. **No seat counts even with `availability.indicator: true`.**

### Working endpoint — gate/boarding-status (no PNR)
```
GET https://mobile-api.delta.com/gate/boarding/boarding-status/{FLIGHT}/{DATE}/{ORIGIN}/{DEST}
Accept: application/json
User-Agent: FlyDelta/24.10.1 (Pixel 7; Android 14; Build/UQ1A.240205.004)
```
Returns boarding zones (visual UI data: name, displayName, gradients, isBoarding). No load data. Returns `{"pollInterval":0}` when flight isn't in boarding window.

### Endpoints that return seat data BUT require auth or PNR

#### `mytrips/getUpgradeAndStandby` (Zulu, JSON) — requires PNR
```
POST https://mobile-api.delta.com/mytrips/getUpgradeAndStandby

{
  "arrivalCity": "LAX",
  "departCity": "ATL",
  "departDate": "20260409",
  "firstName": "JOHN",
  "flightNumber": "996",
  "lastName": "DOE",
  "recordLocator": "ABC123"
}
```
Returns `ASLResponse` with `seatsRemaining`, `totalWaitList`, `passengerStatus`, `standbyPassengers`, `upgradePassengers`, `upgradeSeatRemaining` (per cabin). **All fields required** — `{"errorMessage":"Required fields are missing."}` if any are blank.

#### `getUpgradeAndStandby` (legacy XML) — requires PNR
```
POST https://api.delta.com/api/mobile/getUpgradeAndStandby
Content-Type: application/xml
```
Same data, XML wrapping with `RequestInfo` element. Domain `v2`.

#### `trips/today` / `mytrips/today` — requires PNR
- `trips/today` (legacy XML) on `v3` domain (`api.delta.com/shim2`)
- `mytrips/today` (Zulu JSON) on `aws_mobile_private`
- Body: `TodayTripsRequest` containing `TodayTrip[]` with `confirmationNumber`, `firstName`, `lastName`, `pnrHash`
- Header: `standbySupported: true`
- Returns `MultiTripsResponse` containing `Leg[]`, each leg has `standbyPriorityInfo` of type `UpgradeStandbyModel`:
  ```json
  {
    "seatsRemaining": "12",
    "totalWaitList": "5",
    "currentPosition": null,
    "upgradeSeatRemaining": [
      { "classOfService": "F", "seatsRemaining": 2, "totalWaitList": 3 }
    ],
    "hasFourCabinConfigurationFlag": true,
    "brandIdList": ["FIRST", "DCP", "MAIN", "BE"]
  }
  ```
- Returns 500 "Could not process today trips request" with fake PNR

#### `offers/shop` (booking shop) — requires SkyMiles auth
```
POST https://mobile-api.delta.com/offers/shop
```
Body is `ShopRequest`:
```json
{
  "segments": [{ "departureDate": "...", "origin": "ATL", "destination": "LAX", "connectionAirport": null }],
  "constraints": [],
  "version": "2",
  "tripType": "OW",
  "passenger": { "adultCount": "1" },
  "passengers": [...],
  "priceType": "Revenue",
  "sortBy": "customScore",
  "requestType": "NATIVE_SHOP"
}
```
Returns `FlightSearchShopResultsResponse` containing `ShoppingItinerary[]` → `ShoppingFare[]` with per-fare `seatsAvailableCount` (alt name `seatsRemaining`), `seatsRemainingLabel`, `isSoldOut`, `notOffered`, `availableForSale`, `solutionId`. **MockShopV2Response.json in iOS bundle confirms response shape.**

Domain `aws_mobile_private` — needs SkyMiles auth interceptor. From curl: 444 from Akamai BMP.

#### `mwsb/service/shop` — alternate shop endpoint, requires auth
```
POST https://api.delta.com/mwsb/service/shop
Content-Type: application/json
X-Adapter: mobile
X-APP-ROUTE: SL-RSB
X-OFFER-ROUTE: SL-SHOP
```
Same `ShopRequest` body, same response shape. Domain `shopbook`. From curl: 444 from Akamai (different host, same problem).

Other variants:
- `/service/shop` → 403 "Missing Authentication Token"
- `/offers/shop2` → 401 "Unauthorized"

#### `mbl-flightchange/v1/offers` — flight change shop, requires PNR
- Domain: aws_mobile_private
- Returns ShoppingFare[] with seat counts
- For users changing an existing booking — requires recordLocatorId

#### `seatmap/lsm` — Logical Seat Map
- On the network whitelist (no-auth path), but the variants `proxy/seatmap/lsm` and `seatmap/lsm` both need session context
- Returns `SeatMapResponseModel` with cabin layouts and seat availability
- Body `SeatMapRequestPayload` requires `flightList[]`, `numberOfPassengers`, `recordLocator` (or cart context)

### Network whitelist (`u3/d.java` — endpoints that bypass auth checks)
```
/user/login
/mobile/login
/service/shop
/offers/shop
offers/shop2
flight-status-mobile/schedule
flight-status-mobile/details   ← confirmed working
/mobile/getFlightStatus
/mobile/getFlightStatusByLeg
mwsb/service/itinerarySearch
/pf-ws/authn/flows/
usernameUniqueness
referenceVerification
enrollment
/payment/eDocuments/search
seatmap/lsm
```
Excluded: `/user/loginAndGetDashBrdData`

### Complete domain inventory (Android + iOS combined)

| Domain identifier | URL | Used for | Auth |
|---|---|---|---|
| `aws_mobile_public` | `mobile-api.delta.com` | flight status, gate, supportedVersion, checkin/getCompUpgrade, boarding, travelinfo/inspShopOffers | none |
| `aws_mobile_private` | `mobile-api.delta.com` | offers/shop, mytrips/*, seatmap/*, checkout/*, mbl-flightchange/* | SkyMiles |
| `aws_mobile_login` | `mobile-api.delta.com` | login flows | OAuth |
| `v2` | `api.delta.com/api/mobile` | legacy XML: getFlightStatus, getUpgradeAndStandby, getFlightStatusByLeg, getUpgradeEligibilityInfo, /shop, getPNR | mixed |
| `v3` | `api.delta.com/shim2` | trips/today (legacy XML) | PNR |
| `v4` | `api.delta.com` | CDP automation, feature toggles | none |
| `shopbook` | `api.delta.com/mwsb/service` | mwsb shop, itinerarySearch | auth |
| `acl` | `api.delta.com/mwsb/service` | ACL/IROP | auth |
| `aws_payment` | `enterprisepayments-api.delta.com` | payment | auth |
| `aws_performance_stats` | `flightperfstats-api.delta.com` | flight perf stats (`/flightPerformance/v1/statistics`) | none |
| `aws_travel_policy` | `salespartnersaffiliation-api.delta.com` | corp travel policy | auth |
| `aws_travel_policy_aem` | `mobilecontent.delta.com` | AEM content | none |
| `loyaltyApi` | `loyalty-api.delta.com` | `loyaltyActivity/v2/accountActivity/dashboard` | auth |
| `loyaltyApi2` | `loyalty2-api.delta.com` | `loyaltyProgram/v2/statusTracker/medallionStatus`, `futureActivities`, choice benefits | auth |
| `catalog_api` | `catalog-api-prd.delta.com/prd` | content catalog | none |
| `content` | `content-api.delta.com` | content | none |
| `profile` | `customer-api.delta.com` | profile, save companion, addresses, preferences, GraphQL | auth |
| `assist` (Android) | `concierge-api.delta.com` | GraphQL chatbot, `endpoint=graphql` | auth |
| `assist_realtime` | `wss://concierge-api.delta.com/graphql/realtime` | WebSocket chat | auth |
| **`conversation_agent`** (iOS only) | `conversationalagent-api.delta.com` | newer GraphQL chatbot | auth |
| `conversation_agent_realtime` (iOS only) | `wss://conversationalagent-api.delta.com/graphql/realtime` | WebSocket chat | auth |
| **`choiceBenefitContent`** (iOS only) | `wcmdotcom-api.delta.com` | CMS choice benefits | none |
| `purchase_calculator` | `purchasecalculator-api.delta.com` | trip total | auth |
| `skyclub_tracker` | `loungeamexvisitreport-api.delta.com` | sky club visits | auth |
| `inflight_wifi` | `wifi.delta.com/api` | wifi-only | n/a |
| `aws_error_mapping` | `mobilecontent-dev.delta.com` | error message lookups | none |
| `encrypt_decrypt` | `encryptdecrypt-api.delta.com` | crypto helper | auth |
| `web_server` | `www.delta.com` | website | n/a |
| `cdn` | `www.delta.com/` | CDN | n/a |
| `signin` | `signin.delta.com` | OAuth login | n/a |
| `firebase_url` | `flydelta-mobile.firebaseio.com` | Firebase | n/a |
| `wrapped_deeplink_o` (iOS) | `click.o.delta.com` | deeplinks | n/a |
| `wrapped_deeplink_t` (iOS) | `click.t.delta.com` | deeplinks | n/a |
| **(website only)** `flightinformation-api.delta.com` | — | `flight-status/details`, `flight-status/schedules` | session cookie |
| (website only) `dcomaircraft-api.delta.com` | — | `/flightinformation/fleet` | none |
| (website only) `amenitiesmeals-api.delta.com` | — | `/flight-status/amenities-meals` | none |
| (website only) `predictive-api.delta.com` | — | `/getPredictiveCities/{code}` | none |
| (website only) `rcmd-api.delta.com` | — | `/register` (recommendations) | n/a |

### Every API model with `seatsRemaining` or `totalWaitList`

| Model | Source | Auth | Direct API model? |
|---|---|---|---|
| `ASLResponse.java` | `mytrips/getUpgradeAndStandby` (Zulu) or `getUpgradeAndStandby` (legacy XML) | PNR | yes |
| `UpgradeStandbyModel.java` | embedded as `Leg.standbyPriorityInfo` in `MultiTripsResponse` from `trips/today` | PNR | yes |
| `ShoppingFare.java` | `offers/shop`, `mwsb/service/shop`, `mbl-flightchange/v1/offers` | auth | yes |
| `FareDetailModel.java` | constructed by `FlightChangeFlightDetailsBuilder` from `NativeSearchResultsResponse` + `ShoppingFare` | auth | no — derived |
| `ASLStandby.java` | internal Parcelable, built from `ASLResponse` | PNR | no — internal |
| `ClassOfService.java` | `com.delta.mobile.services.bean.itineraries`, set programmatically (no `@Expose` on field) | PNR | no — not from wire |
| `UpgradeSeatRemaining.java` (×2 — todaymode + asl) | nested in ASLResponse / UpgradeStandbyModel | PNR | nested |
| `CabinAvailability.java` | `getUpgradeEligibilityInfo` | PNR | yes — but Y/N flags only, not counts |

### iOS-specific findings

iOS app: `com.delta.iphone.ver1_7.9.1_und3fined.ipa` (decrypted via und3fined). Bundle: `Payload/FlyDelta.app/`.

Frameworks of interest:
- `FlyDeltaComms.framework` — networking layer
- `Today.framework` / `TodayMode.framework` — Today Mode (where standby data lives)
- `ShopBook.framework` — booking
- `SeatMap.framework` — seat map
- `Concierge.framework` — chatbot
- **`AkamaiBMP.framework`** — confirms Delta uses Akamai BMP with sensor data generation
- `SharedModels.framework` — shared data models with own `environments.json`

iOS-only domain identifiers (added since the Android version was built):
- `conversation_agent` (`conversationalagent-api.delta.com`) — replaces `assist`/`concierge-api`
- `conversation_agent_realtime` — WebSocket
- `choiceBenefitContent` (`wcmdotcom-api.delta.com`)
- `wrapped_deeplink_o` / `wrapped_deeplink_t`

iOS-specific test data files (sample API responses bundled in the app):
- `Frameworks/Today.framework/new_airport_mode.json` — sample `mytrips/today` response with `standbyPriorityInfo: { seatsRemaining: null, currentPosition: null, totalWaitList: null }` per Leg
- `Frameworks/ShopBook.framework/MockShopV2Response.json` — sample shop response with `seatsAvailableCount: 1` per fare
- `getPnrResponse_OneWay_WithInfant.json` — sample PNR response
- `seatmapbffMultipleCabins.json` / `seatmapbffUpperdeck.json` — sample seat map responses
- `check_in_success_response.json` — sample check-in response

iOS-specific Objective-C properties:
- `coachClassSeatsAvailable` (NSString) and `firstClassSeatsAvailable` (NSString) — these match Android's `CabinAvailability.comfortClassAvailableFlag` (W) / `firstClassAvailableFlag` (F). They're Y/N flags, not counts. Sourced from `getUpgradeEligibilityInfo` (PNR-bound).
- `getFlightStatusByFlightNumber:` — Objective-C method. Calls the same legacy V2 `getFlightStatus`/`getFlightStatusByLeg` endpoints. No new wire path.
- `UpgradeStandbyListRequest` (Swift class in `_TtC8FlyDelta25UpgradeStandbyListRequest`) — same as Android's standby flow.

### Decompilation root
```
/Users/m4mini/Desktop/code/airlines/extracted/delta/jadx_out/sources/    (Android)
/Users/m4mini/Desktop/code/airlines/extracted/delta_ios/Payload/FlyDelta.app/    (iOS)
```

### Bottom line on Delta
- **`mobile-api.delta.com/flight-status-mobile/details`** is the only public-without-auth endpoint that returns rich flight data, but it has zero seat counts
- All paths to `seatsRemaining` / `totalWaitList` / `seatsAvailableCount` go through either:
  - PNR-bound endpoints (`getUpgradeAndStandby`, `trips/today`, `mytrips/today`)
  - Auth-bound shop endpoints (`offers/shop`, `mwsb/service/shop`)
- The Akamai BMP framework (in iOS bundle) is the gate that blocks our scripted shop calls
- Possible angles not yet pursued: extract sensor-data generation from `AkamaiBMP.framework` and reproduce in script (multi-day RE effort); search for an anonymous SkyMiles guest token endpoint (not found by name)

---

## 8. ALASKA AIRLINES — partial

**Domain:** `https://www.alaskaair.com`
**Mobile API:** `/1/guestservices/customermobile/`
**Auth:** confirmation code required for load data
**Method:** Playwright (website uses shadow DOM) or curl on mobile API paths

### Step 1 — flight status
```
GET /1/guestservices/customermobile/flights/status/{AIRLINE}/{NUM}/{YYYY-MM-DD}
```
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

{
  "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 (REQUIRES confirmation code)
```
POST /1/guestservices/customermobile/seats/SeatUpgradesByCabinRec/{CONFIRMATION_CODE}

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

### Step 4 — full FlightLoad (requires runtime APIM key)
```
POST /seats/waitlist
```
Returns `FlightLoad` with `authorized`, `available`, `booked`, `checkedIn` per cabin + standby/upgrade lists. The `confirmationCode` field is nullable in the request model, but the APIM key is loaded from Firebase Remote Config at runtime — not statically present in the APK.

### Notes
- Uses Ktor HTTP client (modern Kotlin)
- Mobile API paths start with `/1/guestservices/customermobile/`
- Website uses deep shadow DOM — Playwright automation is complex
- To get the Firebase APIM key: install app on device, capture from traffic
- Alternative: `GET /viewseatmap/seatmap` to count open seats from the map

### `remote_config_defaults.xml` (extracted from APK)
Notable feature flags (defaults — overridden by Firebase Remote Config):
```xml
<entry><key>min_version_code</key><value>0</value></entry>
<entry><key>show_save_to_google_pay</key><value>true</value></entry>
<entry><key>preorder_food_polling_start_days</key><value>-14</value></entry>
<entry><key>seatmap_advisory_enabled</key><value>false</value></entry>
<entry><key>show_health_agreement</key><value>true</value></entry>
<entry><key>health_agreement_info</key><value>https://m.alaskaair.com/healthagreement</value></entry>
<entry><key>is_card_on_file_enabled</key><value>false</value></entry>
<entry><key>is_new_home_page_enabled</key><value>true</value></entry>
<entry><key>is_pay_at_airport_boarding_pass_enabled</key><value>false</value></entry>
<entry><key>is_skip_security_line_enabled</key><value>false</value></entry>
<entry><key>is_bag_tracking_enabled</key><value>false</value></entry>
<entry><key>major_schedule_change_phone_number</key><value>tel:+18778624093</value></entry>
```
(Full list extracted; nothing in defaults exposes the APIM key — it's runtime only.)

---

## 9. FRONTIER AIRLINES — not analyzed

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

---

## 10. BRITISH AIRWAYS — not deeply analyzed

APK extracted but not deep-dived. BA is a oneworld carrier so similar to AA in structure, but the standby/upgrade flow lives in the executive club portal (auth-gated).

---

## Anti-bot protection summary

| Airline | WAF | Bot detection | Difficulty | Working bypass |
|---|---|---|---|---|
| United | Akamai | Akamai BMP | Medium | Playwright session token |
| American | Akamai | Akamai ACF sensor | Medium | Mobile UA `Android/...|AmericanAirlines` |
| Delta | Dynatrace + Akamai BMP | `AkamaiBMP.framework` | High | Mobile UA for `mobile-api`; shop endpoints have no known bypass |
| Spirit | Akamai | CyberFend BMP | Medium | APIM key for GET; POST blocked from scripts |
| JetBlue | Azure APIM | Static API key | Low | Just send the key |
| Korean Air | minimal | minimal | Low | `channel: app` header |
| Emirates | none on flight-status | — | None | Curl |
| Alaska | minimal | minimal | Low | Mobile API paths |

---

## Decrypted keys & secrets

### Spirit Azure APIM
| Env | Decrypted key |
|---|---|
| **Production** | **`c6567af50d544dfbb3bc5dd99c6bb177`** |
| Dev/UAT | `81ffe172c5c741cdac0a2cc13ab19b54` |
| Stage | `daa76fa3c25d423f880b939d56992553` |

Decryption: AES/CBC/PKCS5Padding, SHA-512("1983Miramar")[:16] key, IV `Aw@#EDfTGec3Rtd!`. Source: native lib `libspirit-lib.so`.

### JetBlue Azure APIM
| Use | Key |
|---|---|
| Main API (`az-api.jetblue.com`) | **`49fc015f1ba44abf892d2b8961612378`** |
| Seat map / logging | `a5ee654e981b4577a58264fed9b1669c` |

Captured from web traffic via Playwright.

### Other notable extracted strings
- Delta TrackJS token (front-end): `40c5d1df6a8049dfa571d3e4324bef72` (app `www-nextgen-flight-status`)
- Delta business chat ID: `6431449c-701a-432c-8a47-275b730ac559`
- Delta Adobe app ID: `launch-ENee224a00122947fdbbb787826f1099dc`

---

## Reusable Playwright wrapper

```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 — get flight loads
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 — get waitlist + available seats
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 — flight status
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();
  }
);
```

## Plain HTTP examples (no Playwright)

```bash
# SPIRIT — flight status
curl -X POST "https://api.spirit.com/customermobileprod/2.8.0/v3/GetFlightInfoBI" \
  -H "Content-Type: application/json" \
  -H "Ocp-Apim-Subscription-Key: c6567af50d544dfbb3bc5dd99c6bb177" \
  -H "Platform: Android" \
  -d '{"departureStation":"FLL","arrivalStation":"ATL","departureDate":"2026-04-08"}'

# JETBLUE — flight status
curl "https://az-api.jetblue.com/flight-status/get-by-number?number=524&date=2026-04-08" \
  -H "apikey: 49fc015f1ba44abf892d2b8961612378" \
  -H "Accept: application/json"

# EMIRATES — flight status (zero auth!)
curl "https://www.emirates.com/service/flight-status?departureDate=2026-04-08&flight=221"

# DELTA — flight status (mobile UA required)
curl -X POST "https://mobile-api.delta.com/flight-status-mobile/details" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "User-Agent: FlyDelta/24.10.1 (Pixel 7; Android 14; Build/UQ1A.240205.004)" \
  -d '{"airlineCode":"DL","flightNumber":"996","flightOriginDate":"2026-04-12"}'

# DELTA — gate boarding status (mobile UA required)
curl "https://mobile-api.delta.com/gate/boarding/boarding-status/996/2026-04-12/ATL/LAX" \
  -H "Accept: application/json" \
  -H "User-Agent: FlyDelta/24.10.1 (Pixel 7; Android 14; Build/UQ1A.240205.004)"

# DELTA — schedules (mobile UA required, web variant works too)
curl -X POST "https://flightinformation-api.delta.com/flight-status/schedules" \
  -H "Content-Type: application/json" \
  -H "User-Agent: FlyDelta/24.10.1 (Pixel 7; Android 14; Build/UQ1A.240205.004)" \
  -d '{"flightSegments":[{"flightOriginDate":"2026-04-12","originAirportCode":"ATL","destinationAirportCode":"LAX","airlineCode":"DL"}],"filterOptions":{"schedule":{"directOnlyIndicator":false,"includeElapsedOrAccumulatedTime":true,"flightStops":[],"departureTimeRanges":[],"excludeOtherAirlineInd":true,"connections":[{"connectionPointNum":"","connectionCity":"","minConnectionTime":""}],"startRecord":1,"endRecord":30},"availability":{"indicator":false,"seatTypeCode":"A","onlyRevenueClassInd":false}}}'
```

---

## Rate limits & best practices

- **United:** Token expires in ~30 min. Cache and refresh. No known rate limit.
- **American:** No token. Akamai may throttle if too many requests from same browser context. Rotate browser contexts.
- **Spirit:** APIM key shared across all app users. No known rate limit but don't abuse.
- **JetBlue:** API key never expires. No known rate limit.
- **Korean Air:** No auth. `channel` header required.
- **Delta:** No rate limit observed on `mobile-api` flight status. Shop endpoints will reject before rate-limiting.
- **Emirates:** Zero auth on flight status. No known rate limit.
- **All Playwright airlines:** Reuse browser context across queries to avoid re-establishing sessions. Close and recreate if you get 403s.

---

## Known gaps and what's missing

### Airlines with confirmed load data endpoints but blocked access

| Airline | What exists | What's blocking | Possible path forward |
|---|---|---|---|
| **Alaska** | `POST /seats/waitlist` returns FlightLoad (authorized/available/booked/checkedIn) + standby/upgrade lists | APIM key loaded from Firebase Remote Config at runtime (not in APK) | Install app on device, capture key from traffic, OR use `GET /viewseatmap/seatmap` and count open seats |
| **JetBlue** | `retrievePriorityList` returns capacity/available/booked per cabin (J+Y) + standby count + passenger names | Requires PNR to establish check-in session via `identifyPNR` | Need a valid PNR. `registerClient` works without one but `identifyPNR` validates server-side |
| **Delta** | `getUpgradeAndStandby` returns full standby data; `offers/shop` returns `seatsAvailableCount` per fare | Standby needs PNR + name. Shop needs SkyMiles auth + Akamai BMP sensor data | Either acquire PNR/auth, OR reverse `AkamaiBMP.framework` from iOS bundle to reproduce sensor data generation |
| **Emirates** | `olci/v1/checkin/staffinformation/{pnr}/{lastName}` returns full flight load table (F/J/Y/W cabin counts) | Akamai BMP on `mobileapp.emirates.com` + requires staff travel PNR | Staff travel only — no public load data path |
| **Korean Air** | `flightSeatCount` endpoint exists | Returns 0 — likely needs IBE session (call `removeGarbageSession` first) or only works close to departure | Test with date within 24-48 hrs; call `removeGarbageSession` first |
| **Spirit** | `seatmap` endpoints with availability | Needs JWT token + Akamai bypass (CyberFend sensor data) | Real Android device with Frida hook on rooted device |

### Airlines I haven't deeply analyzed
- **Frontier** — APK not located
- **British Airways** — APK extracted but not analyzed
- **Allegiant** — not pulled
- **Hawaiian** — not pulled
- **Sun Country** — not pulled
- **Air Canada** — not pulled
- **Lufthansa** — not pulled

---

## File structure of `/Users/m4mini/Desktop/code/airlines/`

```
airlines/
├── airlines_request.md                    ← this document
├── AIRLINE_API_SPEC.md                    ← integration spec for app developer
├── API_FINDINGS.md                        ← initial findings doc
├── FLIGHT_LOADS_API.md                    ← flight loads quick-reference
├── package.json / node_modules/           ← Playwright 1.58 + chromium-1208
├── scripts/
│   ├── united_search.js                   ← United Playwright flight search
│   ├── delta_flight_status_test.js        ← Delta flight status endpoint test
│   ├── delta_focused.js                   ← Delta multi-endpoint comparison
│   ├── delta_diag.js                      ← Delta XHR vs fetch diagnostic
│   ├── delta_real_search.js               ← Delta form interaction capture
│   ├── delta_route_form.js                ← Delta route search form
│   ├── delta_form_capture.js              ← Delta form capture v1
│   ├── delta_capture_v2.js, v3.js         ← capture iterations
│   ├── delta_test_correct_body.js         ← Delta body format test
│   ├── delta_test_website_api.js          ← Delta website API test
│   ├── delta_grep_js.js                   ← Delta SPA bundle grep
│   ├── delta_click_through.js             ← Delta detail page click-through
│   └── delta_website_capture.js           ← Delta website capture
├── extracted/
│   ├── aa/jadx_out/sources/               ← American Airlines (Java)
│   ├── alaska/                            ← Alaska Airlines
│   │   ├── apktool_out/res/xml/remote_config_defaults.xml
│   │   └── jadx_out/sources/
│   ├── delta/jadx_out/sources/            ← Delta Android (Java)
│   ├── delta_ios/Payload/FlyDelta.app/    ← Delta iOS (decrypted)
│   │   ├── FlyDelta                       ← main Mach-O binary
│   │   ├── Frameworks/
│   │   │   ├── FlyDeltaComms.framework/
│   │   │   ├── Today.framework/
│   │   │   ├── TodayMode.framework/
│   │   │   ├── ShopBook.framework/
│   │   │   ├── SeatMap.framework/
│   │   │   ├── Concierge.framework/
│   │   │   ├── AkamaiBMP.framework/       ← bot manager sensor lib
│   │   │   └── SharedModels.framework/
│   │   └── Hybrid/Resources/assets/scripts/app/environments.json
│   ├── jetblue/                           ← JetBlue Android
│   ├── korean/jadx_out/sources/           ← Korean Air
│   ├── spirit/                            ← Spirit Android
│   │   ├── jadx_out/sources/              ← decompiled Java
│   │   └── native/lib/                    ← native libraries (libspirit-lib.so)
│   ├── united/jadx_out/sources/           ← United Android
│   ├── emirates/                          ← Emirates Android
│   └── ba/                                ← British Airways
├── captures/                              ← Screenshots and traffic captures
├── com.delta.iphone.ver1_7.9.1_und3fined.ipa  ← decrypted iOS Delta app
├── com.alaskaairlines.android.apk
├── alaska_patched.apk
├── com.jetblue.JetBlueAndroid.xapk
├── com.koreanair.passenger.xapk
├── com.ba.mobile.apk
└── com.emirates.ek.android.xapk
```

---

## Open questions / next steps

1. **Delta Akamai BMP bypass** — extract sensor data generation from `AkamaiBMP.framework` (iOS) and reproduce in Node/Python. This would unlock `offers/shop` and the website's `flight-status/details` endpoint. Multi-day reverse-engineering effort.

2. **Delta SkyMiles guest token** — search for an anonymous SkyMiles auth flow. Searched for `guestToken`, `anonymousToken`, `guestSession` in both Android and iOS — no hits. Might be hidden under a different name.

3. **Alaska APIM key** — install Alaska app on a real device, capture traffic to extract the Firebase Remote Config APIM key. Once obtained, the `/seats/waitlist` endpoint would return full FlightLoad data.

4. **Frontier APK** — locate and pull. Frontier is likely similar to Spirit (ULCC, no standby).

5. **British Airways deep dive** — pull oneworld flight inventory endpoints (similar to AA but in BA's portal).

6. **JetBlue PNR-less path** — investigate whether `mobilecheckin.jetblue.com/checkin/registerClient` actually validates PNR or just creates a session. If session creation is enough, we might be able to call `retrievePriorityList` with crafted parameters.

7. **Korean Air `flightSeatCount`** — test with dates within 24-48 hrs of departure to confirm whether the 0 result is date-driven or session-driven.

8. **Spirit POST endpoints** — only path is real device with Frida hook on CyberFend.

---

## Lessons learned

- **The simplest approach beats playwright theater.** For the Delta `flight-status-mobile/details` endpoint, the answer was `curl -X POST <url> -H "User-Agent: FlyDelta/..." -d '<3-field-body>'`. I spent multiple iterations on Playwright form interactions when the answer was just curl with the right UA.
- **The User-Agent matters more than expected.** Akamai BMP often whitelists requests by mobile UA pattern. American Airlines wouldn't return data without `Android/2025.31 Pixel 7|14|...|AmericanAirlines`. Delta wouldn't return data without `FlyDelta/24.10.1 (Pixel 7; Android 14; Build/UQ1A.240205.004)`.
- **Anonymous token beats anonymous session.** United's `/api/auth/anonymous-token` is the cleanest pattern in the industry — short-lived bearer token with no PII required.
- **iOS bundles include test data.** Apps ship sample API responses for offline development (`MockShopV2Response.json`, `new_airport_mode.json`). These reveal exact response shapes without needing to call the real API.
- **`@SerializedName` is the source of truth.** When grepping for API field names, always check `@SerializedName` annotations first — that's what comes over the wire. `@Expose` without `@SerializedName` falls back to the Java field name.
- **The "public auth whitelist" file is gold.** Delta's `u3/d.java` `NetworkSecurityConstants` lists endpoints that bypass auth checks. Every Akamai-protected airline has something similar.
- **Read both Android and iOS.** They're built from different source trees and the iOS app sometimes has newer endpoints, framework names, or domain identifiers that the Android app doesn't.

---

*Document compiled 2026-04-11. Findings span multiple sessions of APK reverse engineering, Playwright traffic capture, and direct API testing. All endpoint URLs, body formats, and response shapes verified against actual decompiled source unless marked otherwise.*
