Airline integration work: AirlineLoadService updates, docs, JSX scripts

- 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>
This commit is contained in:
Trey t
2026-04-24 23:21:30 -05:00
parent 1e74552184
commit 6005146e75
26 changed files with 61519 additions and 1334 deletions
+398
View File
@@ -0,0 +1,398 @@
# Airline API Integration Guide
Drop-in reference for integrating flight load / seat availability data from 11 airlines. Each section tells you **what works today, how to call it, what you get back, and what's blocked**. Verified 2026-04-12.
---
## 1. United Airlines — WORKING (best US data)
**What you get:** Per-cabin capacity, booked, available, revenue standby, space-available, upgrade/standby passenger lists with names.
**Auth:** Anonymous token (~30 min lifetime), Playwright required (TLS fingerprinting blocks curl).
**Flow:**
```
GET https://www.united.com/api/auth/anonymous-token
→ { data: { token: { hash: "DAAAA..." } } }
GET https://www.united.com/api/flightstatus/upgradeListExtended
?flightNumber=2238&flightDate=2026-04-08&fromAirportCode=EWR
Headers:
x-authorization-api: bearer {token.hash}
Accept: application/json
```
**Response shape:**
```json
{
"pbts": [
{ "cabin": "Front", "capacity": 50, "booked": 50, "revenueStandby": 0, "sa": 5, "waitList": 0 },
{ "cabin": "Middle", "capacity": 24, "booked": 16 },
{ "cabin": "Rear", "capacity": 202, "booked": 164, "revenueStandby": 2, "sa": 4 }
],
"front": { "cleared": [...], "standby": [...] }
}
```
**Derived:** `availableSeats = capacity - booked`, `loadFactor = booked / capacity`.
**Cabin mapping:** `Front` = Polaris/First, `Middle` = Premium Plus, `Rear` = Economy.
**Other useful endpoints:**
- `GET /api/flightstatus/status/{num}/{date}/{origin}/{dest}?carrierCode=UA` — gates, times, equipment
- `GET /api/flightstatus/seatmap/{num}/{date}/{origin}/{dest}?carrierCode=UA` — seat map
---
## 2. American Airlines — WORKING
**What you get:** Waitlist per class (First, Standby), seats available per class with semantic color, passenger names + order.
**Auth:** None — but mobile User-Agent is mandatory. Direct curl blocked by TLS fingerprinting; use Playwright.
**Required headers (on browser context):**
```
User-Agent: Android/2025.31 Pixel 7|14|1080|2400|1.0|AmericanAirlines
x-clientid: MOBILE
Device-ID: {any-uuid}
Accept: application/json
```
**The UA format is strict.** Any deviation returns `{"error":["Invalid user-agent header"]}` from Akamai.
**Flow:**
```
# Step 1 — find the flight
GET https://cdn.flyaa.aa.com/apiv2/mobile-flifo/flightSchedules/v1.0
?origin=DFW&destination=IAH&departureDay=9&departureMonth=4
&searchType=schedule&noOfFlightsToDisplay=20
# Step 2 — get waitlist + seats
GET https://cdn.flyaa.aa.com/api/mobile/loyalty/waitlist/v1.2
?carrierCode=AA&flightNumber={num}&departureDate={YYYY-MM-DD}
&originAirportCode={origin}&destinationAirportCode={dest}
Headers: x-referrer: fs
```
**Response shape:**
```json
{
"waitList": [
{
"listName": "First",
"seatsAvailableValue": 1,
"seatsAvailableSemanticColor": "failure",
"passengers": [
{ "order": 1, "displayName": "BRI, K", "cleared": false, "seat": null }
]
},
{ "listName": "Standby", "seatsAvailableValue": 45, "seatsAvailableSemanticColor": "success", "passengers": [...] }
]
}
```
`seatsAvailableSemanticColor`: `success` (many), `warning` (few), `failure` (≤1).
---
## 3. Alaska Airlines — WORKING (easiest integration)
**What you get:** Seat map with per-seat status + `AvailableSeats` per cabin, **full standby + upgrade waitlists with passenger names**, capacity, cabin configuration.
**Auth:** Static APIM key (decrypted from APK). **Plain curl works — no Playwright needed.**
**Key:** `de1d0ff837444468a5ea868945aab738`
**Header:** `Ocp-Apim-Subscription-Key: de1d0ff837444468a5ea868945aab738`
### Seat map (per-seat availability)
```bash
curl "https://apis.alaskaair.com/1/guestservices/customermobile/viewseatmap/seatmap\
?flightnumber=308&departureairport=SEA&arrivalairport=PSP&departuredate=2026-04-13" \
-H "Ocp-Apim-Subscription-Key: de1d0ff837444468a5ea868945aab738"
```
Per-seat status: `OCCD` (occupied), `OPEN`, `PCLA` (premium class), `PREM` (premium). Returns `AvailableSeats` per cabin section.
### Standby + upgrade waitlist (the prize — **no PNR needed**)
```bash
curl -X POST "https://apis.alaskaair.com/1/guestservices/customermobile/seats/waitlist" \
-H "Ocp-Apim-Subscription-Key: de1d0ff837444468a5ea868945aab738" \
-H "Content-Type: application/json" \
-d '{
"marketedByAirlineCode":"AS",
"departureAirportCode":"SEA",
"departureLocalDate":"2026-04-12",
"flightNumber":"308",
"confirmationCode": null
}'
```
**Response includes:**
- `StandbyList.FlightLoad.Authorized` — cabin capacity
- `StandbyList.FlightLoad.PremiumClassConfigured` — first-class exists
- `StandbyList.Passengers[]``DisplayName`, `Position`, `Seat`, `UpgradedToPC`
- `UpgradeList.FlightLoad.Authorized` — first-class capacity
- `UpgradeList.Passengers[]` — upgrade waitlist
**Key insight:** `confirmationCode: null` is accepted. Works for past, current, and future flights (tested 2+ weeks out).
### Flight status
```
GET /1/guestservices/customermobile/flights/status/AS/{num}/{YYYY-MM-DD}
```
---
## 4. JSX — WORKING (per-flight seat counts)
**What you get:** Per-flight `availableCount` per fare class, full schedule, route network.
**Auth:** Anonymous JWT (15 min idle), Playwright required for the initial token call (Akamai).
**Carrier code:** X2 (ICAO: XSR).
### Step 1 — get token (Playwright)
```javascript
await page.goto('https://www.jsx.com');
const { data } = await page.evaluate(async () => {
const r = await fetch('https://api.jsx.com/api/nsk/v2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ applicationName: 'IBE', credentials: { channelType: 'DigitalWeb' } })
});
return r.json();
});
const token = data.token; // raw JWT, no "Bearer " prefix
```
### Step 2 — per-flight availability (GraphQL)
```
POST https://api.jsx.com/api/v2/graph/searchAvailability
Headers:
authorization: {token}
Content-Type: application/json
Body:
{
"cachedResults": false,
"query": "{ availabilityv5(request: { criteria: [{ dates: { beginDate: \"2026-04-15\", endDate: \"2026-04-15\" }, stations: { originStationCodes: [\"BUR\"], destinationStationCodes: [\"LAS\"] } }], passengers: { types: [{ count: 1, type: \"ADT\" }] }, codes: { currencyCode: \"USD\" } }) { results { trips { date journeysAvailableByMarket { key value { journeyKey stops designator { arrival departure destination origin } segments { identifier { identifier carrierCode } } fares { details { availableCount classOfService productClass passengerFares { fareAmount } } } } } } } } }"
}
```
Returns per-flight seat counts per fare class (sample: BUR→LAS 6 flights, 12 seats each).
### Step 3 — low fare calendar (REST, simpler)
```
GET https://api.jsx.com/api/nsk/v1/availability/lowfare/estimate
?Origin=BUR&Destination=LAS&StartDate=2026-04-15&EndDate=2026-04-18
&IncludeTaxesAndFees=true&PassengerCount=1&CurrencyCode=USD
Headers: authorization: {token}
```
### Route network (GraphQL)
`POST /api/v2/graph/primaryResources` — returns all markets + station coords.
Script ready at `scripts/jsx_availability.js`.
---
## 5. Spirit Airlines — PARTIAL (status only, no standby)
**What you get:** Flight status, station/route data. **No standby — Spirit is a ULCC and doesn't run standby lists.**
**Auth:** Static APIM key (decrypted). Plain curl for GETs; POSTs mostly blocked by Akamai CyberFend sensor.
**Key:** `c6567af50d544dfbb3bc5dd99c6bb177`
```bash
curl -X POST "https://api.spirit.com/customermobileprod/2.8.0/v3/GetFlightInfoBI" \
-H "Ocp-Apim-Subscription-Key: c6567af50d544dfbb3bc5dd99c6bb177" \
-H "Content-Type: application/json" \
-H "Platform: Android" \
-d '{"departureStation":"FLL","arrivalStation":"ATL","departureDate":"2026-04-08"}'
```
Seat maps require JWT + CyberFend sensor data (real device + Frida hook only).
---
## 6. JetBlue — PARTIAL (status yes, loads need PNR)
**What you get without PNR:** Flight status, full route database (12MB of origin/dest pairs, Mint/seasonal flags).
**What you get with a PNR:** Per-cabin capacity, confirmed pax, authorized seats, standby/waitlist counts, passenger names.
**Auth:** Static API key.
**Keys:**
- Main: `49fc015f1ba44abf892d2b8961612378`
- Seat map: `a5ee654e981b4577a58264fed9b1669c`
- MYB/PNR: `45804e33f26b44d1b144090af2788abf`
### Flight status (no PNR)
```bash
curl "https://az-api.jetblue.com/flight-status/get-by-number?number=524&date=2026-04-08" \
-H "apikey: 49fc015f1ba44abf892d2b8961612378"
```
### Route database (no PNR)
```
GET https://azrest.jetblue.com/od/od-service/routes
```
### Priority list / loads (PNR required)
```
POST https://jbrest.jetblue.com/lookup/itinerary
Body: { "fName":"JOHN", "lName":"DOE", "from":"LAX", "pnr":"ABC123", "channelID":"M" }
→ returns jbSessionId
POST https://jbrest.jetblue.com/prioritylist/getPriorityList?jbSessionId={id}
→ numberOfCapacityJ/Y, numberOfAvailableSeatsJ/Y, numberOfAuthorizedSeatsJ/Y,
numberOfStandbyPassengers, numberOfWaitListedPassengers, priorityListPassengers[]
```
**No public load path exists without a real PNR.**
---
## 7. Korean Air — PARTIAL
**What you get:** Flight status, route availability. `flightSeatCount` endpoint exists but returns 0 for far-out dates (works best within 2448 hrs of departure).
**Auth:** None. `channel` header required (`app` for flight search, `pc` for seat count).
```
POST https://www.koreanair.com/api/fs/scheduleFlightSearch/flight/status/app
Headers: channel: app
Body: {"departureDate":"20260408","flightNumber":"017","searchOption":"FLTNUM",
"departureLocationCode":"","arrivalLocationCode":""}
POST https://www.koreanair.com/api/et/ibeSupport/flightSeatCount
Headers: channel: pc
Body: {"carrierCode":"KE","flightNumber":"017","departureAirport":"ICN",
"arrivalAirport":"LAX","departureDate":"20260409"}
```
---
## 8. Emirates — PARTIAL (zero-auth status only)
**What you get:** Flight status with gates, times, equipment — zero auth, zero headers.
**Staff load tables exist but are staff-travel only (PNR + last name required).**
```bash
curl "https://www.emirates.com/service/flight-status?departureDate=2026-04-08&flight=221"
```
---
## 9. Delta — BLOCKED (status only, no public load path)
**What you get:** Rich flight status (gates, times, equipment, amenities per cabin). **Zero seat counts anywhere public.**
**Auth:** Mobile User-Agent only for status. Shop/standby data requires SkyMiles auth + Akamai BMP sensor (blocked from scripts).
**Mobile UA:** `FlyDelta/24.10.1 (Pixel 7; Android 14; Build/UQ1A.240205.004)`
```bash
curl -X POST "https://mobile-api.delta.com/flight-status-mobile/details" \
-H "Content-Type: 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"}'
```
**Load data endpoints that exist but are blocked:**
- `POST /mytrips/getUpgradeAndStandby` — needs PNR + name
- `POST /offers/shop` — returns `seatsAvailableCount` per fare, needs SkyMiles auth + SPA session flow
- `POST /mwsb/service/shop` — same data, same auth requirement
The SkyMiles login alone is not enough — the SPA sets session state that the backend validates. Direct API calls fail even with valid auth.
---
## 10. British Airways — BLOCKED (no public load data)
Flight availability search exists (`/sc4/baflt-paa/rs/v1/flightavailability/search`) but returns bookable fares, not load factors. Seat availability is SOAP and needs a booking reference.
**Public OAuth:** `POST https://oauth.baplc.com/grant` with `client_id=baflt`
**Legacy SOAP:** `Authorization: Basic cHVibGljOnB1YmxpYw==` (public:public)
No standby/waitlist endpoint found.
---
## 11. Qantas — BLOCKED
Seatmap endpoint returns `isSeatAvailable`/`isSeatOccupied` per seat, but requires a valid boarding pass (productId + surname). All upgrade endpoints require auth. Akamai BMP with native sensor SDK makes automation impractical.
---
## 12. Lufthansa — BLOCKED (developer API has maps, not occupancy)
Main API behind Cloudflare WAF (403 from curl). Official developer API at `api.lufthansa.com/v1/` has seat map **layouts** but not occupancy. Seat recommendation API needs PNR.
Register: https://developer.lufthansa.com/member/register (free, 6 req/sec, 1000/hr).
Same backend powers Lufthansa, SWISS, Austrian, Brussels.
---
## Anti-bot & auth cheat sheet
| Airline | Bypass | Effort |
|------------|--------------------|---------|
| Alaska | APIM key header | Lowest (curl works) |
| Emirates | none | Lowest (curl works) |
| Spirit | APIM key (GET only)| Low (curl works) |
| JetBlue | apikey header | Low (curl works) |
| Korean Air | `channel` header | Low (Playwright or curl) |
| JSX | Playwright → JWT | Medium |
| United | Playwright → token | Medium |
| American | Playwright + mobile UA | Medium |
| Delta | mobile UA for status; shop blocked | Low/High |
| BA / Qantas / Lufthansa | — | N/A (no public load data) |
---
# Summary — what to build against
## Tier 1: Plug-and-play (integrate today)
| Airline | Data quality | Call pattern |
|---------|--------------|--------------|
| **Alaska** | ★★★★★ seat map + full standby/upgrade lists w/ names, no PNR | Plain `curl` with APIM key |
| **United** | ★★★★★ per-cabin loads + cleared upgrades + standby list | Playwright token + API fetch |
| **American** | ★★★★ waitlist + seats per class w/ pax names | Playwright w/ mobile UA |
| **JSX** | ★★★★ per-flight seat counts per fare class | Playwright JWT + GraphQL |
These four are the core of any flight-load product. Alaska is the easiest to integrate (pure HTTP), United returns the richest data, American is close behind, JSX is the only public source for per-flight counts on a Navitaire-hosted carrier.
## Tier 2: Status only (useful, but no seat data)
- **Spirit** — status/routes, no standby (ULCC)
- **Emirates** — status, zero auth
- **Korean Air** — status; `flightSeatCount` returns 0 far out
- **JetBlue** — status + route DB; loads need PNR
- **Delta** — rich status, no seat counts anywhere public
## Tier 3: Blocked / not useful
- **BA, Qantas, Lufthansa** — no public load data. Qantas/BA need booking ref; Lufthansa dev API is layouts only.
## Recommended product shape
1. Start with Alaska (easiest, 15 min to wire up).
2. Add United for the standby/upgrade killer feature (needs Playwright worker).
3. Layer in American for the third major US carrier.
4. JSX as a bonus — only route pairs that JSX serves (private terminals).
5. For Delta/JetBlue: show flight status only, note "seat data unavailable" unless you have a PNR.
6. Use Emirates/Korean Air/Spirit for status on international/ULCC routes.
## Shared integration notes
- **Cache aggressively** — all four Tier 1 sources return stable data per flight-date; a 60-second cache dramatically cuts load.
- **Token management** — United (30 min) and JSX (15 min idle) need refresh logic.
- **Playwright workers** — run one persistent browser context per airline; reuse across requests.
- **Alaska is the exception** — no browser, no token, just HTTP.
Full endpoint-by-endpoint reference: `airlines_request.md` (1692 lines, same directory).