- AirlineLoadService: pass airport DB for timezone-aware date strings, add browser-shaped headers for United, expand JetBlue/Alaska/Emirates signatures to take origin, log/parse fixes for Korean Air. - FlightsApp: build AirlineLoadService with the airport DB and inject it. - JSX: continued WebView-based fetcher work plus updated JSX_NOTES. - Docs: add AIRLINE_INTEGRATION_GUIDE.md, drop the old AIRLINE_API_SPEC.md, add api_docs/ (StaffTraveler reverse-engineering captures + findings). - Scripts: jsx_cdp_probe, jsx_live_monitor, jsx_swift_smoke for JSX protocol exploration. - .gitignore: exclude airlines/ (local-only APK/IPA reverse-engineering). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
53 KiB
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
- Plain HTTP (curl-able): Spirit, JetBlue, Emirates, Korean Air, Delta
flight-status-mobile/details - Playwright with mobile User-Agent: American (
cdn.flyaa.aa.com), Deltaflight-status/details(web variant) - Playwright with full browser session + Akamai cookies: United (
united.com), Deltaoffers/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 |
| 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:
{ "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):
{
"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, tailGET /api/flightstatus/seatmap/{NUM}/{DATE}/{ORIGIN}/{DEST}?carrierCode=UA— interactive seat mapPOST /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 searchPOST /SeatEngine/PreviewSeatMap— seat map with availabilityGET /StandByList/GetStandbyList— standby list (load data)GET /UpgradeList/GetUpgradeList— upgrade waitlistPOST /Shopping/GetONTimePerformence— on-time performancePOST /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 ...notAuthorization- 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:
{
"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.2endpoint is the waitlist data — noteloyaltyin the path - Add
x-referrer: fsheader 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:
{
"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:
{
"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
registerClientworks with just an airport code; the gate isidentifyPNRwhich 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
channelheader required:appfor flight search,pcfor seat countflightSeatCountis real but only useful close to departure- Try with
departureDateas 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:
{
"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 availablestaffPax.passengers[]— staff passenger list with check-in statusflights[]— 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) onv3domain (api.delta.com/shim2)mytrips/today(Zulu JSON) onaws_mobile_private- Body:
TodayTripsRequestcontainingTodayTrip[]withconfirmationNumber,firstName,lastName,pnrHash - Header:
standbySupported: true - Returns
MultiTripsResponsecontainingLeg[], each leg hasstandbyPriorityInfoof typeUpgradeStandbyModel:{ "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:
{
"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/lsmandseatmap/lsmboth need session context - Returns
SeatMapResponseModelwith cabin layouts and seat availability - Body
SeatMapRequestPayloadrequiresflightList[],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 layerToday.framework/TodayMode.framework— Today Mode (where standby data lives)ShopBook.framework— bookingSeatMap.framework— seat mapConcierge.framework— chatbotAkamaiBMP.framework— confirms Delta uses Akamai BMP with sensor data generationSharedModels.framework— shared data models with ownenvironments.json
iOS-only domain identifiers (added since the Android version was built):
conversation_agent(conversationalagent-api.delta.com) — replacesassist/concierge-apiconversation_agent_realtime— WebSocketchoiceBenefitContent(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— samplemytrips/todayresponse withstandbyPriorityInfo: { seatsRemaining: null, currentPosition: null, totalWaitList: null }per LegFrameworks/ShopBook.framework/MockShopV2Response.json— sample shop response withseatsAvailableCount: 1per faregetPnrResponse_OneWay_WithInfant.json— sample PNR responseseatmapbffMultipleCabins.json/seatmapbffUpperdeck.json— sample seat map responsescheck_in_success_response.json— sample check-in response
iOS-specific Objective-C properties:
coachClassSeatsAvailable(NSString) andfirstClassSeatsAvailable(NSString) — these match Android'sCabinAvailability.comfortClassAvailableFlag(W) /firstClassAvailableFlag(F). They're Y/N flags, not counts. Sourced fromgetUpgradeEligibilityInfo(PNR-bound).getFlightStatusByFlightNumber:— Objective-C method. Calls the same legacy V2getFlightStatus/getFlightStatusByLegendpoints. 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/detailsis the only public-without-auth endpoint that returns rich flight data, but it has zero seat counts- All paths to
seatsRemaining/totalWaitList/seatsAvailableCountgo through either:- PNR-bound endpoints (
getUpgradeAndStandby,trips/today,mytrips/today) - Auth-bound shop endpoints (
offers/shop,mwsb/service/shop)
- PNR-bound endpoints (
- 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.frameworkand 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:
[
{
"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/seatmapto count open seats from the map
remote_config_defaults.xml (extracted from APK)
Notable feature flags (defaults — overridden by Firebase Remote Config):
<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/... |
| 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(appwww-nextgen-flight-status) - Delta business chat ID:
6431449c-701a-432c-8a47-275b730ac559 - Delta Adobe app ID:
launch-ENee224a00122947fdbbb787826f1099dc
Reusable Playwright wrapper
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)
# 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.
channelheader required. - Delta: No rate limit observed on
mobile-apiflight 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
-
Delta Akamai BMP bypass — extract sensor data generation from
AkamaiBMP.framework(iOS) and reproduce in Node/Python. This would unlockoffers/shopand the website'sflight-status/detailsendpoint. Multi-day reverse-engineering effort. -
Delta SkyMiles guest token — search for an anonymous SkyMiles auth flow. Searched for
guestToken,anonymousToken,guestSessionin both Android and iOS — no hits. Might be hidden under a different name. -
Alaska APIM key — install Alaska app on a real device, capture traffic to extract the Firebase Remote Config APIM key. Once obtained, the
/seats/waitlistendpoint would return full FlightLoad data. -
Frontier APK — locate and pull. Frontier is likely similar to Spirit (ULCC, no standby).
-
British Airways deep dive — pull oneworld flight inventory endpoints (similar to AA but in BA's portal).
-
JetBlue PNR-less path — investigate whether
mobilecheckin.jetblue.com/checkin/registerClientactually validates PNR or just creates a session. If session creation is enough, we might be able to callretrievePriorityListwith crafted parameters. -
Korean Air
flightSeatCount— test with dates within 24-48 hrs of departure to confirm whether the 0 result is date-driven or session-driven. -
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/detailsendpoint, the answer wascurl -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 withoutFlyDelta/24.10.1 (Pixel 7; Android 14; Build/UQ1A.240205.004). - Anonymous token beats anonymous session. United's
/api/auth/anonymous-tokenis 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. @SerializedNameis the source of truth. When grepping for API field names, always check@SerializedNameannotations first — that's what comes over the wire.@Exposewithout@SerializedNamefalls back to the Java field name.- The "public auth whitelist" file is gold. Delta's
u3/d.javaNetworkSecurityConstantslists 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.