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:
@@ -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, 1–2 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 24–48 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).
|
||||
Reference in New Issue
Block a user